diff options
Diffstat (limited to 'crates')
139 files changed, 6751 insertions, 5917 deletions
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index 68ba7308..040a4405 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -22,7 +22,7 @@ az = "1.2" chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] } comemo = "0.3" csv = "1" -ecow = "0.1" +ecow = { version = "0.1.2", features = ["serde"] } hayagriva = "0.3.2" hypher = "0.1.3" icu_properties = { version = "1.2.0", features = ["serde"] } diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs index 83ecac5d..9043e1f1 100644 --- a/crates/typst-library/src/compute/calc.rs +++ b/crates/typst-library/src/compute/calc.rs @@ -8,62 +8,65 @@ use typst::eval::{Module, Scope}; use crate::prelude::*; -/// A module with computational functions. -pub fn module() -> Module { +/// Hook up all calculation definitions. +pub(super) fn define(global: &mut Scope) { + global.category("calculate"); + global.define_module(module()); +} + +/// A module with calculation definitions. +fn module() -> Module { let mut scope = Scope::new(); - scope.define("abs", abs_func()); - scope.define("pow", pow_func()); - scope.define("exp", exp_func()); - scope.define("sqrt", sqrt_func()); - scope.define("sin", sin_func()); - scope.define("cos", cos_func()); - scope.define("tan", tan_func()); - scope.define("asin", asin_func()); - scope.define("acos", acos_func()); - scope.define("atan", atan_func()); - scope.define("atan2", atan2_func()); - scope.define("sinh", sinh_func()); - scope.define("cosh", cosh_func()); - scope.define("tanh", tanh_func()); - scope.define("log", log_func()); - scope.define("ln", ln_func()); - scope.define("fact", fact_func()); - scope.define("perm", perm_func()); - scope.define("binom", binom_func()); - scope.define("gcd", gcd_func()); - scope.define("lcm", lcm_func()); - scope.define("floor", floor_func()); - scope.define("ceil", ceil_func()); - scope.define("trunc", trunc_func()); - scope.define("fract", fract_func()); - scope.define("round", round_func()); - scope.define("clamp", clamp_func()); - scope.define("min", min_func()); - scope.define("max", max_func()); - scope.define("even", even_func()); - scope.define("odd", odd_func()); - scope.define("rem", rem_func()); - scope.define("quo", quo_func()); + scope.category("calculate"); + scope.define_func::<abs>(); + scope.define_func::<pow>(); + scope.define_func::<exp>(); + scope.define_func::<sqrt>(); + scope.define_func::<sin>(); + scope.define_func::<cos>(); + scope.define_func::<tan>(); + scope.define_func::<asin>(); + scope.define_func::<acos>(); + scope.define_func::<atan>(); + scope.define_func::<atan2>(); + scope.define_func::<sinh>(); + scope.define_func::<cosh>(); + scope.define_func::<tanh>(); + scope.define_func::<log>(); + scope.define_func::<ln>(); + scope.define_func::<fact>(); + scope.define_func::<perm>(); + scope.define_func::<binom>(); + scope.define_func::<gcd>(); + scope.define_func::<lcm>(); + scope.define_func::<floor>(); + scope.define_func::<ceil>(); + scope.define_func::<trunc>(); + scope.define_func::<fract>(); + scope.define_func::<round>(); + scope.define_func::<clamp>(); + scope.define_func::<min>(); + scope.define_func::<max>(); + scope.define_func::<even>(); + scope.define_func::<odd>(); + scope.define_func::<rem>(); + scope.define_func::<quo>(); scope.define("inf", f64::INFINITY); scope.define("nan", f64::NAN); scope.define("pi", std::f64::consts::PI); scope.define("tau", std::f64::consts::TAU); scope.define("e", std::f64::consts::E); - Module::new("calc").with_scope(scope) + Module::new("calc", scope) } /// Calculates the absolute value of a numeric value. /// -/// ## Example { #example } /// ```example /// #calc.abs(-5) \ /// #calc.abs(5pt - 2cm) \ /// #calc.abs(2fr) /// ``` -/// -/// Display: Absolute -/// Category: calculate -#[func] +#[func(title = "Absolute")] pub fn abs( /// The value whose absolute value to calculate. value: ToAbs, @@ -87,21 +90,17 @@ cast! { /// Raises a value to some exponent. /// -/// ## Example { #example } /// ```example /// #calc.pow(2, 3) /// ``` -/// -/// Display: Power -/// Category: calculate -#[func] +#[func(title = "Power")] pub fn pow( + /// The callsite span. + span: Span, /// The base of the power. base: Num, /// The exponent of the power. exponent: Spanned<Num>, - /// The callsite span. - span: Span, ) -> SourceResult<Num> { match exponent.v { _ if exponent.v.float() == 0.0 && base.float() == 0.0 => { @@ -142,19 +141,15 @@ pub fn pow( /// Raises a value to some exponent of e. /// -/// ## Example { #example } /// ```example /// #calc.exp(1) /// ``` -/// -/// Display: Exponential -/// Category: calculate -#[func] +#[func(title = "Exponential")] pub fn exp( - /// The exponent of the power. - exponent: Spanned<Num>, /// The callsite span. span: Span, + /// The exponent of the power. + exponent: Spanned<Num>, ) -> SourceResult<f64> { match exponent.v { Num::Int(i) if i32::try_from(i).is_err() => { @@ -176,15 +171,11 @@ pub fn exp( /// Extracts the square root of a number. /// -/// ## Example { #example } /// ```example /// #calc.sqrt(16) \ /// #calc.sqrt(2.5) /// ``` -/// -/// Display: Square Root -/// Category: calculate -#[func] +#[func(title = "Square Root")] pub fn sqrt( /// The number whose square root to calculate. Must be non-negative. value: Spanned<Num>, @@ -200,16 +191,12 @@ pub fn sqrt( /// When called with an integer or a float, they will be interpreted as /// radians. /// -/// ## Example { #example } /// ```example /// #assert(calc.sin(90deg) == calc.sin(-270deg)) /// #calc.sin(1.5) \ /// #calc.sin(90deg) /// ``` -/// -/// Display: Sine -/// Category: calculate -#[func] +#[func(title = "Sine")] pub fn sin( /// The angle whose sine to calculate. angle: AngleLike, @@ -226,16 +213,12 @@ pub fn sin( /// When called with an integer or a float, they will be interpreted as /// radians. /// -/// ## Example { #example } /// ```example /// #calc.cos(90deg) \ /// #calc.cos(1.5) \ /// #calc.cos(90deg) /// ``` -/// -/// Display: Cosine -/// Category: calculate -#[func] +#[func(title = "Cosine")] pub fn cos( /// The angle whose cosine to calculate. angle: AngleLike, @@ -252,15 +235,11 @@ pub fn cos( /// When called with an integer or a float, they will be interpreted as /// radians. /// -/// ## Example { #example } /// ```example /// #calc.tan(1.5) \ /// #calc.tan(90deg) /// ``` -/// -/// Display: Tangent -/// Category: calculate -#[func] +#[func(title = "Tangent")] pub fn tan( /// The angle whose tangent to calculate. angle: AngleLike, @@ -274,15 +253,11 @@ pub fn tan( /// Calculates the arcsine of a number. /// -/// ## Example { #example } /// ```example /// #calc.asin(0) \ /// #calc.asin(1) /// ``` -/// -/// Display: Arcsine -/// Category: calculate -#[func] +#[func(title = "Arcsine")] pub fn asin( /// The number whose arcsine to calculate. Must be between -1 and 1. value: Spanned<Num>, @@ -296,15 +271,11 @@ pub fn asin( /// Calculates the arccosine of a number. /// -/// ## Example { #example } /// ```example /// #calc.acos(0) \ /// #calc.acos(1) /// ``` -/// -/// Display: Arccosine -/// Category: calculate -#[func] +#[func(title = "Arccosine")] pub fn acos( /// The number whose arcsine to calculate. Must be between -1 and 1. value: Spanned<Num>, @@ -318,15 +289,11 @@ pub fn acos( /// Calculates the arctangent of a number. /// -/// ## Example { #example } /// ```example /// #calc.atan(0) \ /// #calc.atan(1) /// ``` -/// -/// Display: Arctangent -/// Category: calculate -#[func] +#[func(title = "Arctangent")] pub fn atan( /// The number whose arctangent to calculate. value: Num, @@ -338,15 +305,11 @@ pub fn atan( /// /// The arguments are `(x, y)`, not `(y, x)`. /// -/// ## Example { #example } /// ```example /// #calc.atan2(1, 1) \ /// #calc.atan2(-2, -3) /// ``` -/// -/// Display: Four-quadrant Arctangent -/// Category: calculate -#[func] +#[func(title = "Four-quadrant Arctangent")] pub fn atan2( /// The X coordinate. x: Num, @@ -358,15 +321,11 @@ pub fn atan2( /// Calculates the hyperbolic sine of a hyperbolic angle. /// -/// ## Example { #example } /// ```example /// #calc.sinh(0) \ /// #calc.sinh(1.5) /// ``` -/// -/// Display: Hyperbolic sine -/// Category: calculate -#[func] +#[func(title = "Hyperbolic Sine")] pub fn sinh( /// The hyperbolic angle whose hyperbolic sine to calculate. value: f64, @@ -376,15 +335,11 @@ pub fn sinh( /// Calculates the hyperbolic cosine of a hyperbolic angle. /// -/// ## Example { #example } /// ```example /// #calc.cosh(0) \ /// #calc.cosh(1.5) /// ``` -/// -/// Display: Hyperbolic cosine -/// Category: calculate -#[func] +#[func(title = "Hyperbolic Cosine")] pub fn cosh( /// The hyperbolic angle whose hyperbolic cosine to calculate. value: f64, @@ -394,15 +349,11 @@ pub fn cosh( /// Calculates the hyperbolic tangent of an hyperbolic angle. /// -/// ## Example { #example } /// ```example /// #calc.tanh(0) \ /// #calc.tanh(1.5) /// ``` -/// -/// Display: Hyperbolic tangent -/// Category: calculate -#[func] +#[func(title = "Hyperbolic Tangent")] pub fn tanh( /// The hyperbolic angle whose hyperbolic tangent to calculate. value: f64, @@ -414,23 +365,19 @@ pub fn tanh( /// /// If the base is not specified, the logarithm is calculated in base 10. /// -/// ## Example { #example } /// ```example /// #calc.log(100) /// ``` -/// -/// Display: Logarithm -/// Category: calculate -#[func] +#[func(title = "Logarithm")] pub fn log( + /// The callsite span. + span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned<Num>, /// The base of the logarithm. May not be zero. #[named] #[default(Spanned::new(10.0, Span::detached()))] base: Spanned<f64>, - /// The callsite span. - span: Span, ) -> SourceResult<f64> { let number = value.v.float(); if number <= 0.0 { @@ -460,19 +407,15 @@ pub fn log( /// Calculates the natural logarithm of a number. /// -/// ## Example { #example } /// ```example /// #calc.ln(calc.e) /// ``` -/// -/// Display: Natural Logarithm -/// Category: calculate -#[func] +#[func(title = "Natural Logarithm")] pub fn ln( - /// The number whose logarithm to calculate. Must be strictly positive. - value: Spanned<Num>, /// The callsite span. span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, ) -> SourceResult<f64> { let number = value.v.float(); if number <= 0.0 { @@ -489,14 +432,10 @@ pub fn ln( /// Calculates the factorial of a number. /// -/// ## Example { #example } /// ```example /// #calc.fact(5) /// ``` -/// -/// Display: Factorial -/// Category: calculate -#[func] +#[func(title = "Factorial")] pub fn fact( /// The number whose factorial to calculate. Must be non-negative. number: u64, @@ -506,14 +445,10 @@ pub fn fact( /// Calculates a permutation. /// -/// ## Example { #example } /// ```example /// #calc.perm(10, 5) /// ``` -/// -/// Display: Permutation -/// Category: calculate -#[func] +#[func(title = "Permutation")] pub fn perm( /// The base number. Must be non-negative. base: u64, @@ -547,14 +482,10 @@ fn fact_impl(start: u64, end: u64) -> Option<i64> { /// Calculates a binomial coefficient. /// -/// ## Example { #example } /// ```example /// #calc.binom(10, 5) /// ``` -/// -/// Display: Binomial -/// Category: calculate -#[func] +#[func(title = "Binomial")] pub fn binom( /// The upper coefficient. Must be non-negative. n: u64, @@ -588,14 +519,10 @@ fn binom_impl(n: u64, k: u64) -> Option<i64> { /// Calculates the greatest common divisor of two integers. /// -/// ## Example { #example } /// ```example /// #calc.gcd(7, 42) /// ``` -/// -/// Display: Greatest Common Divisor -/// Category: calculate -#[func] +#[func(title = "Greatest Common Divisor")] pub fn gcd( /// The first integer. a: i64, @@ -614,14 +541,10 @@ pub fn gcd( /// Calculates the least common multiple of two integers. /// -/// ## Example { #example } /// ```example /// #calc.lcm(96, 13) /// ``` -/// -/// Display: Least Common Multiple -/// Category: calculate -#[func] +#[func(title = "Least Common Multiple")] pub fn lcm( /// The first integer. a: i64, @@ -642,15 +565,11 @@ pub fn lcm( /// /// If the number is already an integer, it is returned unchanged. /// -/// ## Example { #example } /// ```example /// #assert(calc.floor(3.14) == 3) /// #assert(calc.floor(3) == 3) /// #calc.floor(500.1) /// ``` -/// -/// Display: Round down -/// Category: calculate #[func] pub fn floor( /// The number to round down. @@ -666,15 +585,11 @@ pub fn floor( /// /// If the number is already an integer, it is returned unchanged. /// -/// ## Example { #example } /// ```example /// #assert(calc.ceil(3.14) == 4) /// #assert(calc.ceil(3) == 3) /// #calc.ceil(500.1) /// ``` -/// -/// Display: Round up -/// Category: calculate #[func] pub fn ceil( /// The number to round up. @@ -690,16 +605,12 @@ pub fn ceil( /// /// If the number is already an integer, it is returned unchanged. /// -/// ## Example { #example } /// ```example /// #assert(calc.trunc(3) == 3) /// #assert(calc.trunc(-3.7) == -3) /// #assert(calc.trunc(15.9) == 15) /// ``` -/// -/// Display: Truncate -/// Category: calculate -#[func] +#[func(title = "Truncate")] pub fn trunc( /// The number to truncate. value: Num, @@ -714,15 +625,11 @@ pub fn trunc( /// /// If the number is an integer, returns `0`. /// -/// ## Example { #example } /// ```example /// #assert(calc.fract(3) == 0) /// #calc.fract(-3.1) /// ``` -/// -/// Display: Fractional -/// Category: calculate -#[func] +#[func(title = "Fractional")] pub fn fract( /// The number to truncate. value: Num, @@ -737,15 +644,11 @@ pub fn fract( /// /// Optionally, a number of decimal places can be specified. /// -/// ## Example { #example } /// ```example /// #assert(calc.round(3.14) == 3) /// #assert(calc.round(3.5) == 4) /// #calc.round(3.1415, digits: 2) /// ``` -/// -/// Display: Round -/// Category: calculate #[func] pub fn round( /// The number to round. @@ -767,15 +670,11 @@ pub fn round( /// Clamps a number between a minimum and maximum value. /// -/// ## Example { #example } /// ```example /// #assert(calc.clamp(5, 0, 10) == 5) /// #assert(calc.clamp(5, 6, 10) == 6) /// #calc.clamp(5, 0, 4) /// ``` -/// -/// Display: Clamp -/// Category: calculate #[func] pub fn clamp( /// The number to clamp. @@ -793,44 +692,36 @@ pub fn clamp( /// Determines the minimum of a sequence of values. /// -/// ## Example { #example } /// ```example /// #calc.min(1, -3, -5, 20, 3, 6) \ /// #calc.min("typst", "in", "beta") /// ``` -/// -/// Display: Minimum -/// Category: calculate -#[func] +#[func(title = "Minimum")] pub fn min( + /// The callsite span. + span: Span, /// The sequence of values from which to extract the minimum. /// Must not be empty. #[variadic] values: Vec<Spanned<Value>>, - /// The callsite span. - span: Span, ) -> SourceResult<Value> { minmax(span, values, Ordering::Less) } /// Determines the maximum of a sequence of values. /// -/// ## Example { #example } /// ```example /// #calc.max(1, -3, -5, 20, 3, 6) \ /// #calc.max("typst", "in", "beta") /// ``` -/// -/// Display: Maximum -/// Category: calculate -#[func] +#[func(title = "Maximum")] pub fn max( + /// The callsite span. + span: Span, /// The sequence of values from which to extract the maximum. /// Must not be empty. #[variadic] values: Vec<Spanned<Value>>, - /// The callsite span. - span: Span, ) -> SourceResult<Value> { minmax(span, values, Ordering::Greater) } @@ -858,15 +749,11 @@ fn minmax( /// Determines whether an integer is even. /// -/// ## Example { #example } /// ```example /// #calc.even(4) \ /// #calc.even(5) \ /// #range(10).filter(calc.even) /// ``` -/// -/// Display: Even -/// Category: calculate #[func] pub fn even( /// The number to check for evenness. @@ -877,15 +764,11 @@ pub fn even( /// Determines whether an integer is odd. /// -/// ## Example { #example } /// ```example /// #calc.odd(4) \ /// #calc.odd(5) \ /// #range(10).filter(calc.odd) /// ``` -/// -/// Display: Odd -/// Category: calculate #[func] pub fn odd( /// The number to check for oddness. @@ -896,15 +779,11 @@ pub fn odd( /// Calculates the remainder of two numbers. /// -/// ## Example { #example } /// ```example /// #calc.rem(20, 6) \ /// #calc.rem(1.75, 0.5) /// ``` -/// -/// Display: Remainder -/// Category: calculate -#[func] +#[func(title = "Remainder")] pub fn rem( /// The dividend of the remainder. dividend: Num, @@ -919,15 +798,11 @@ pub fn rem( /// Calculates the quotient of two numbers. /// -/// ## Example { #example } /// ```example /// #calc.quo(14, 5) \ /// #calc.quo(3.46, 0.5) /// ``` -/// -/// Display: Quotient -/// Category: calculate -#[func] +#[func(title = "Quotient")] pub fn quo( /// The dividend of the quotient. dividend: Num, @@ -949,7 +824,7 @@ pub enum Num { } impl Num { - pub fn apply2( + fn apply2( self, other: Self, int: impl FnOnce(i64, i64) -> i64, @@ -961,7 +836,7 @@ impl Num { } } - pub fn apply3( + fn apply3( self, other: Self, third: Self, @@ -974,7 +849,7 @@ impl Num { } } - pub fn float(self) -> f64 { + fn float(self) -> f64 { match self { Self::Int(v) => v as f64, Self::Float(v) => v, diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs deleted file mode 100644 index 6ea8bd82..00000000 --- a/crates/typst-library/src/compute/construct.rs +++ /dev/null @@ -1,1015 +0,0 @@ -use std::num::NonZeroI64; -use std::str::FromStr; - -use time::{Month, PrimitiveDateTime}; - -use typst::eval::{Bytes, Datetime, Duration, Module, Plugin, Reflect, Regex}; - -use crate::prelude::*; - -/// Converts a value to an integer. -/// -/// - Booleans are converted to `0` or `1`. -/// - Floats are floored to the next 64-bit integer. -/// - Strings are parsed in base 10. -/// -/// ## Example { #example } -/// ```example -/// #int(false) \ -/// #int(true) \ -/// #int(2.7) \ -/// #{ int("27") + int("4") } -/// ``` -/// -/// Display: Integer -/// Category: construct -#[func] -pub fn int( - /// The value that should be converted to an integer. - value: ToInt, -) -> i64 { - value.0 -} - -/// A value that can be cast to an integer. -pub struct ToInt(i64); - -cast! { - ToInt, - v: bool => Self(v as i64), - v: f64 => Self(v as i64), - v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?), - v: i64 => Self(v), -} - -/// Converts a value to a float. -/// -/// - Booleans are converted to `0.0` or `1.0`. -/// - Integers are converted to the closest 64-bit float. -/// - Ratios are divided by 100%. -/// - Strings are parsed in base 10 to the closest 64-bit float. -/// Exponential notation is supported. -/// -/// ## Example { #example } -/// ```example -/// #float(false) \ -/// #float(true) \ -/// #float(4) \ -/// #float(40%) \ -/// #float("2.7") \ -/// #float("1e5") -/// ``` -/// -/// Display: Float -/// Category: construct -#[func] -pub fn float( - /// The value that should be converted to a float. - value: ToFloat, -) -> f64 { - value.0 -} - -/// A value that can be cast to a float. -pub struct ToFloat(f64); - -cast! { - ToFloat, - v: bool => Self(v as i64 as f64), - v: i64 => Self(v as f64), - v: Ratio => Self(v.get()), - v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?), - v: f64 => Self(v), -} - -/// Creates a grayscale color. -/// -/// ## Example { #example } -/// ```example -/// #for x in range(250, step: 50) { -/// box(square(fill: luma(x))) -/// } -/// ``` -/// -/// Display: Luma -/// Category: construct -#[func] -pub fn luma( - /// The gray component. - gray: Component, -) -> Color { - LumaColor::new(gray.0).into() -} - -/// Creates an RGB(A) color. -/// -/// The color is specified in the sRGB color space. -/// -/// ## Example { #example } -/// ```example -/// #square(fill: rgb("#b1f2eb")) -/// #square(fill: rgb(87, 127, 230)) -/// #square(fill: rgb(25%, 13%, 65%)) -/// ``` -/// -/// Display: RGB -/// Category: construct -#[func] -pub fn rgb( - /// The color in hexadecimal notation. - /// - /// Accepts three, four, six or eight hexadecimal digits and optionally - /// a leading hashtag. - /// - /// If this string is given, the individual components should not be given. - /// - /// ```example - /// #text(16pt, rgb("#239dad"))[ - /// *Typst* - /// ] - /// ``` - #[external] - hex: EcoString, - /// The red component. - #[external] - red: Component, - /// The green component. - #[external] - green: Component, - /// The blue component. - #[external] - blue: Component, - /// The alpha component. - #[external] - alpha: Component, - /// The arguments. - args: Args, -) -> SourceResult<Color> { - let mut args = args; - Ok(if let Some(string) = args.find::<Spanned<EcoString>>()? { - match RgbaColor::from_str(&string.v) { - Ok(color) => color.into(), - Err(msg) => bail!(string.span, "{msg}"), - } - } else { - let Component(r) = args.expect("red component")?; - let Component(g) = args.expect("green component")?; - let Component(b) = args.expect("blue component")?; - let Component(a) = args.eat()?.unwrap_or(Component(255)); - RgbaColor::new(r, g, b, a).into() - }) -} - -/// An integer or ratio component. -pub struct Component(u8); - -cast! { - Component, - v: i64 => match v { - 0 ..= 255 => Self(v as u8), - _ => bail!("number must be between 0 and 255"), - }, - v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - bail!("ratio must be between 0% and 100%"); - }, -} - -/// Creates a new datetime. -/// -/// You can specify the [datetime]($type/datetime) using a year, month, day, -/// hour, minute, and second. You can also get the current date with -/// [`datetime.today`]($func/datetime.today). -/// -/// ## Example -/// ```example -/// #let date = datetime( -/// year: 2012, -/// month: 8, -/// day: 3, -/// ) -/// -/// #date.display() \ -/// #date.display( -/// "[day].[month].[year]" -/// ) -/// ``` -/// -/// ## Format -/// _Note_: Depending on which components of the datetime you specify, Typst -/// will store it in one of the following three ways: -/// * If you specify year, month and day, Typst will store just a date. -/// * If you specify hour, minute and second, Typst will store just a time. -/// * If you specify all of year, month, day, hour, minute and second, Typst -/// will store a full datetime. -/// -/// Depending on how it is stored, the [`display`]($type/datetime.display) -/// method will choose a different formatting by default. -/// -/// Display: Datetime -/// Category: construct -#[func] -#[scope( - scope.define("today", datetime_today_func()); - scope -)] -pub fn datetime( - /// The year of the datetime. - #[named] - year: Option<YearComponent>, - /// The month of the datetime. - #[named] - month: Option<MonthComponent>, - /// The day of the datetime. - #[named] - day: Option<DayComponent>, - /// The hour of the datetime. - #[named] - hour: Option<HourComponent>, - /// The minute of the datetime. - #[named] - minute: Option<MinuteComponent>, - /// The second of the datetime. - #[named] - second: Option<SecondComponent>, -) -> StrResult<Datetime> { - let time = match (hour, minute, second) { - (Some(hour), Some(minute), Some(second)) => { - match time::Time::from_hms(hour.0, minute.0, second.0) { - Ok(time) => Some(time), - Err(_) => bail!("time is invalid"), - } - } - (None, None, None) => None, - _ => bail!("time is incomplete"), - }; - - let date = match (year, month, day) { - (Some(year), Some(month), Some(day)) => { - match time::Date::from_calendar_date(year.0, month.0, day.0) { - Ok(date) => Some(date), - Err(_) => bail!("date is invalid"), - } - } - (None, None, None) => None, - _ => bail!("date is incomplete"), - }; - - Ok(match (date, time) { - (Some(date), Some(time)) => { - Datetime::Datetime(PrimitiveDateTime::new(date, time)) - } - (Some(date), None) => Datetime::Date(date), - (None, Some(time)) => Datetime::Time(time), - (None, None) => { - bail!("at least one of date or time must be fully specified") - } - }) -} - -pub struct YearComponent(i32); -pub struct MonthComponent(Month); -pub struct DayComponent(u8); -pub struct HourComponent(u8); -pub struct MinuteComponent(u8); -pub struct SecondComponent(u8); - -cast! { - YearComponent, - v: i32 => Self(v), -} - -cast! { - MonthComponent, - v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) -} - -cast! { - DayComponent, - v: u8 => Self(v), -} - -cast! { - HourComponent, - v: u8 => Self(v), -} - -cast! { - MinuteComponent, - v: u8 => Self(v), -} - -cast! { - SecondComponent, - v: u8 => Self(v), -} - -/// Returns the current date. -/// -/// Refer to the documentation of the [`display`]($type/datetime.display) method -/// for details on how to affect the formatting of the date. -/// -/// ## Example -/// ```example -/// Today's date is -/// #datetime.today().display(). -/// ``` -/// -/// Display: Today -/// Category: construct -#[func] -pub fn datetime_today( - /// An offset to apply to the current UTC date. If set to `{auto}`, the - /// offset will be the local offset. - #[named] - #[default] - offset: Smart<i64>, - /// The virtual machine. - vt: &mut Vt, -) -> StrResult<Datetime> { - Ok(vt - .world - .today(offset.as_custom()) - .ok_or("unable to get the current date")?) -} - -/// Creates a new duration. -/// -/// You can specify the [duration]($type/duration) using weeks, days, hours, -/// minutes and seconds. You can also get a duration by subtracting two -/// [datetimes]($type/datetime). -/// -/// ## Example -/// ```example -/// #duration( -/// days: 3, -/// hours: 12, -/// ).hours() -/// ``` -/// -/// Display: Duration -/// Category: construct -#[func] -pub fn duration( - /// The number of seconds. - #[named] - #[default(0)] - seconds: i64, - /// The number of minutes. - #[named] - #[default(0)] - minutes: i64, - /// The number of hours. - #[named] - #[default(0)] - hours: i64, - /// The number of days. - #[named] - #[default(0)] - days: i64, - /// The number of weeks. - #[named] - #[default(0)] - weeks: i64, -) -> Duration { - Duration::from( - time::Duration::seconds(seconds) - + time::Duration::minutes(minutes) - + time::Duration::hours(hours) - + time::Duration::days(days) - + time::Duration::weeks(weeks), - ) -} - -/// Creates a CMYK color. -/// -/// This is useful if you want to target a specific printer. The conversion -/// to RGB for display preview might differ from how your printer reproduces -/// the color. -/// -/// ## Example { #example } -/// ```example -/// #square( -/// fill: cmyk(27%, 0%, 3%, 5%) -/// ) -/// ``` -/// -/// Display: CMYK -/// Category: construct -#[func] -pub fn cmyk( - /// The cyan component. - cyan: RatioComponent, - /// The magenta component. - magenta: RatioComponent, - /// The yellow component. - yellow: RatioComponent, - /// The key component. - key: RatioComponent, -) -> Color { - CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into() -} - -/// A component that must be a ratio. -pub struct RatioComponent(u8); - -cast! { - RatioComponent, - v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - bail!("ratio must be between 0% and 100%"); - }, -} - -/// A module with functions operating on colors. -pub fn color_module() -> Module { - let mut scope = Scope::new(); - scope.define("mix", mix_func()); - Module::new("color").with_scope(scope) -} - -/// Create a color by mixing two or more colors. -/// -/// ## Example { #example } -/// ```example -/// #set block(height: 20pt, width: 100%) -/// #block(fill: color.mix(red, blue)) -/// #block(fill: color.mix(red, blue, space: "srgb")) -/// #block(fill: color.mix((red, 70%), (blue, 30%))) -/// #block(fill: color.mix(red, blue, white)) -/// ``` -/// -/// _Note:_ This function must be specified as `color.mix`, not just `mix`. -/// Currently, `color` is a module, but it is designed to be forward compatible -/// with a future `color` type. -/// -/// Display: Mix -/// Category: construct -#[func] -pub fn mix( - /// The colors, optionally with weights, specified as a pair (array of - /// length two) of color and weight (float or ratio). - /// - /// The weights do not need to add to `{100%}`, they are relative to the - /// sum of all weights. - #[variadic] - colors: Vec<WeightedColor>, - /// The color space to mix in. By default, this happens in a perceptual - /// color space (Oklab). - #[named] - #[default(ColorSpace::Oklab)] - space: ColorSpace, -) -> StrResult<Color> { - Color::mix(colors, space) -} - -/// Creates a custom symbol with modifiers. -/// -/// ## Example { #example } -/// ```example -/// #let envelope = symbol( -/// "🖂", -/// ("stamped", "🖃"), -/// ("stamped.pen", "🖆"), -/// ("lightning", "🖄"), -/// ("fly", "🖅"), -/// ) -/// -/// #envelope -/// #envelope.stamped -/// #envelope.stamped.pen -/// #envelope.lightning -/// #envelope.fly -/// ``` -/// -/// Display: Symbol -/// Category: construct -#[func] -pub fn symbol( - /// The variants of the symbol. - /// - /// Can be a just a string consisting of a single character for the - /// modifierless variant or an array with two strings specifying the modifiers - /// and the symbol. Individual modifiers should be separated by dots. When - /// displaying a symbol, Typst selects the first from the variants that have - /// all attached modifiers and the minimum number of other modifiers. - #[variadic] - variants: Vec<Spanned<Variant>>, - /// The callsite span. - span: Span, -) -> SourceResult<Symbol> { - let mut list = Vec::new(); - if variants.is_empty() { - bail!(span, "expected at least one variant"); - } - for Spanned { v, span } in variants { - if list.iter().any(|(prev, _)| &v.0 == prev) { - bail!(span, "duplicate variant"); - } - list.push((v.0, v.1)); - } - Ok(Symbol::runtime(list.into_boxed_slice())) -} - -/// A value that can be cast to a symbol. -pub struct Variant(EcoString, char); - -cast! { - Variant, - c: char => Self(EcoString::new(), c), - array: Array => { - let mut iter = array.into_iter(); - match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), - _ => bail!("point array must contain exactly two entries"), - } - }, -} - -/// Converts a value to a string. -/// -/// - Integers are formatted in base 10. This can be overridden with the -/// optional `base` parameter. -/// - Floats are formatted in base 10 and never in exponential notation. -/// - From labels the name is extracted. -/// - Bytes are decoded as UTF-8. -/// -/// If you wish to convert from and to Unicode code points, see -/// [`str.to-unicode`]($func/str.to-unicode) and -/// [`str.from-unicode`]($func/str.from-unicode). -/// -/// ## Example { #example } -/// ```example -/// #str(10) \ -/// #str(4000, base: 16) \ -/// #str(2.7) \ -/// #str(1e8) \ -/// #str(<intro>) -/// ``` -/// -/// Display: String -/// Category: construct -#[func] -#[scope( - scope.define("to-unicode", str_to_unicode_func()); - scope.define("from-unicode", str_from_unicode_func()); - scope -)] -pub fn str( - /// The value that should be converted to a string. - value: ToStr, - /// The base (radix) to display integers in, between 2 and 36. - #[named] - #[default(Spanned::new(10, Span::detached()))] - base: Spanned<i64>, -) -> SourceResult<Str> { - Ok(match value { - ToStr::Str(s) => { - if base.v != 10 { - bail!(base.span, "base is only supported for integers"); - } - s - } - ToStr::Int(n) => { - if base.v < 2 || base.v > 36 { - bail!(base.span, "base must be between 2 and 36"); - } - int_to_base(n, base.v).into() - } - }) -} - -/// A value that can be cast to a string. -pub enum ToStr { - /// A string value ready to be used as-is. - Str(Str), - /// An integer about to be formatted in a given base. - Int(i64), -} - -cast! { - ToStr, - v: i64 => Self::Int(v), - v: f64 => Self::Str(format_str!("{}", v)), - v: Label => Self::Str(v.0.into()), - v: Bytes => Self::Str( - std::str::from_utf8(&v) - .map_err(|_| "bytes are not valid utf-8")? - .into() - ), - v: Str => Self::Str(v), -} - -/// Format an integer in a base. -fn int_to_base(mut n: i64, base: i64) -> EcoString { - if n == 0 { - return "0".into(); - } - - // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`. - // So we can only use the built-in for decimal, not bin/oct/hex. - if base == 10 { - return eco_format!("{n}"); - } - - // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long. - const SIZE: usize = 65; - let mut digits = [b'\0'; SIZE]; - let mut i = SIZE; - - // It's tempting to take the absolute value, but this will fail for i64::MIN. - // Instead, we turn n negative, as -i64::MAX is perfectly representable. - let negative = n < 0; - if n > 0 { - n = -n; - } - - while n != 0 { - let digit = char::from_digit(-(n % base) as u32, base as u32); - i -= 1; - digits[i] = digit.unwrap_or('?') as u8; - n /= base; - } - - if negative { - i -= 1; - digits[i] = b'-'; - } - - std::str::from_utf8(&digits[i..]).unwrap_or_default().into() -} - -/// Converts a character into its corresponding code point. -/// -/// ## Example -/// ```example -/// #str.to-unicode("a") \ -/// #"a\u{0300}".codepoints().map(str.to-unicode) -/// ``` -/// -/// Display: String To Unicode -/// Category: construct -#[func] -pub fn str_to_unicode( - /// The character that should be converted. - value: char, -) -> u32 { - value.into() -} - -/// Converts a Unicode code point into its corresponding string. -/// -/// ```example -/// #str.from-unicode(97) -/// ``` -/// -/// Display: String From Unicode -/// Category: construct -#[func] -pub fn str_from_unicode( - /// The code point that should be converted. - value: CodePoint, -) -> Str { - format_str!("{}", value.0) -} - -/// The numeric representation of a single unicode code point. -pub struct CodePoint(char); - -cast! { - CodePoint, - v: i64 => { - Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else( - || eco_format!("{:#x} is not a valid codepoint", v), - )?) - }, -} - -/// Creates a regular expression from a string. -/// -/// The result can be used as a -/// [show rule selector]($styling/#show-rules) and with -/// [string methods]($type/string) like `find`, `split`, and `replace`. -/// -/// See [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#syntax). -/// -/// ## Example { #example } -/// ```example -/// // Works with show rules. -/// #show regex("\d+"): set text(red) -/// -/// The numbers 1 to 10. -/// -/// // Works with string methods. -/// #("a,b;c" -/// .split(regex("[,;]"))) -/// ``` -/// -/// Display: Regex -/// Category: construct -#[func] -pub fn regex( - /// The regular expression as a string. - /// - /// Most regex escape sequences just work because they are not valid Typst - /// escape sequences. To produce regex escape sequences that are also valid in - /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim - /// backslash, you would need to write `{regex("\\\\")}`. - /// - /// If you need many escape sequences, you can also create a raw element - /// and extract its text to use it for your regular expressions: - /// ```{regex(`\d+\.\d+\.\d+`.text)}```. - regex: Spanned<EcoString>, -) -> SourceResult<Regex> { - Regex::new(®ex.v).at(regex.span) -} - -/// Converts a value to bytes. -/// -/// - Strings are encoded in UTF-8. -/// - Arrays of integers between `{0}` and `{255}` are converted directly. The -/// dedicated byte representation is much more efficient than the array -/// representation and thus typically used for large byte buffers (e.g. image -/// data). -/// -/// ```example -/// #bytes("Hello 😃") \ -/// #bytes((123, 160, 22, 0)) -/// ``` -/// -/// Display: Bytes -/// Category: construct -#[func] -pub fn bytes( - /// The value that should be converted to bytes. - value: ToBytes, -) -> Bytes { - value.0 -} - -/// A value that can be cast to bytes. -pub struct ToBytes(Bytes); - -cast! { - ToBytes, - v: Str => Self(v.as_bytes().into()), - v: Array => Self(v.iter() - .map(|v| match v { - Value::Int(byte @ 0..=255) => Ok(*byte as u8), - Value::Int(_) => bail!("number must be between 0 and 255"), - value => Err(<u8 as Reflect>::error(value)), - }) - .collect::<Result<Vec<u8>, _>>()? - .into() - ), - v: Bytes => Self(v), -} - -/// Creates a label from a string. -/// -/// Inserting a label into content attaches it to the closest previous element -/// that is not a space. Then, the element can be [referenced]($func/ref) and -/// styled through the label. -/// -/// ## Example { #example } -/// ```example -/// #show <a>: set text(blue) -/// #show label("b"): set text(red) -/// -/// = Heading <a> -/// *Strong* #label("b") -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: You can create a label by enclosing -/// its name in angle brackets. This works both in markup and code. -/// -/// Display: Label -/// Category: construct -#[func] -pub fn label( - /// The name of the label. - name: EcoString, -) -> Label { - Label(name) -} - -/// Converts a value to an array. -/// -/// Note that this function is only intended for conversion of a collection-like -/// value to an array, not for creation of an array from individual items. Use -/// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead. -/// -/// ```example -/// #let hi = "Hello 😃" -/// #array(bytes(hi)) -/// ``` -/// -/// Display: Array -/// Category: construct -#[func] -pub fn array( - /// The value that should be converted to an array. - value: ToArray, -) -> Array { - value.0 -} - -/// A value that can be cast to bytes. -pub struct ToArray(Array); - -cast! { - ToArray, - v: Bytes => Self(v.iter().map(|&b| Value::Int(b as i64)).collect()), - v: Array => Self(v), -} - -/// Creates an array consisting of consecutive integers. -/// -/// If you pass just one positional parameter, it is interpreted as the `end` of -/// the range. If you pass two, they describe the `start` and `end` of the -/// range. -/// -/// ## Example { #example } -/// ```example -/// #range(5) \ -/// #range(2, 5) \ -/// #range(20, step: 4) \ -/// #range(21, step: 4) \ -/// #range(5, 2, step: -1) -/// ``` -/// -/// Display: Range -/// Category: construct -#[func] -pub fn range( - /// The start of the range (inclusive). - #[external] - #[default] - start: i64, - /// The end of the range (exclusive). - #[external] - end: i64, - /// The distance between the generated numbers. - #[named] - #[default(NonZeroI64::new(1).unwrap())] - step: NonZeroI64, - /// The arguments. - args: Args, -) -> SourceResult<Array> { - let mut args = args; - let first = args.expect::<i64>("end")?; - let (start, end) = match args.eat::<i64>()? { - Some(second) => (first, second), - None => (0, first), - }; - - let step = step.get(); - - let mut x = start; - let mut array = Array::new(); - - while x.cmp(&end) == 0.cmp(&step) { - array.push(Value::Int(x)); - x += step; - } - - Ok(array) -} - -/// Loads a WebAssembly plugin. -/// -/// This is **advanced functionality** and not to be confused with -/// [Typst packages]($scripting/#packages). -/// -/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin -/// functions may accept multiple [byte buffers]($type/bytes) as arguments and -/// return a single byte buffer. They should typically be wrapped in idiomatic -/// Typst functions that perform the necessary conversions between native Typst -/// types and bytes. -/// -/// Plugins run in isolation from your system, which means that printing, -/// reading files, or anything like that will not be supported for security -/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit -/// shared WebAssembly library. Many compilers will use the -/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. -/// emscripten), which allows printing, reading files, etc. This ABI will not -/// directly work with Typst. You will either need to compile to a different -/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/blob/master/wasi-stub). -/// -/// ## Example { #example } -/// ```example -/// #let myplugin = plugin("hello.wasm") -/// #let concat(a, b) = str( -/// myplugin.concatenate( -/// bytes(a), -/// bytes(b), -/// ) -/// ) -/// -/// #concat("hello", "world") -/// ``` -/// -/// ## Protocol { #protocol } -/// To be used as a plugin, a WebAssembly module must conform to the following -/// protocol: -/// -/// ### Exports { #exports } -/// A plugin module can export functions to make them callable from Typst. To -/// conform to the protocol, an exported function should: -/// -/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as -/// lengths, so `usize/size_t` may be preferable), and return one 32-bit -/// integer. -/// -/// - The function should first allocate a buffer `buf` of length -/// `a_1 + a_2 + ... + a_n`, and then call -/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. -/// -/// - The `a_1` first bytes of the buffer now constitute the first argument, the -/// `a_2` next bytes the second argument, and so on. -/// -/// - The function can now do its job with the arguments and produce an output -/// buffer. Before returning, it should call -/// `wasm_minimal_protocol_send_result_to_host` to send its result back to the -/// host. -/// -/// - To signal success, the function should return `0`. -/// -/// - To signal an error, the function should return `1`. The written buffer is -/// then interpreted as an UTF-8 encoded error message. -/// -/// ### Imports { #imports } -/// Plugin modules need to import two functions that are provided by the runtime. -/// (Types and functions are described using WAT syntax.) -/// -/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` -/// -/// Writes the arguments for the current function into a plugin-allocated -/// buffer. When a plugin function is called, it -/// [receives the lengths](#exported-functions) of its input buffers as -/// arguments. It should then allocate a buffer whose capacity is at least the -/// sum of these lengths. It should then call this function with a `ptr` to -/// the buffer to fill it with the arguments, one after another. -/// -/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` -/// -/// Sends the output of the current function to the host (Typst). The first -/// parameter shall be a pointer to a buffer (`ptr`), while the second is the -/// length of that buffer (`len`). The memory pointed at by `ptr` can be freed -/// immediately after this function returns. If the message should be -/// interpreted as an error message, it should be encoded as UTF-8. -/// -/// ## Resources { #resources } -/// For more resources, check out the -/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). -/// It contains: -/// -/// - A list of example plugin implementations and a test runner for these -/// examples -/// - Wrappers to help you write your plugin in Rust (Zig wrapper in -/// development) -/// - A stubber for WASI -/// -/// Display: Plugin -/// Category: construct -#[func] -pub fn plugin( - /// Path to a WebAssembly file. - path: Spanned<EcoString>, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult<Plugin> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - Plugin::new(data).at(span) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_to_base() { - assert_eq!(&int_to_base(0, 10), "0"); - assert_eq!(&int_to_base(0, 16), "0"); - assert_eq!(&int_to_base(0, 36), "0"); - assert_eq!( - &int_to_base(i64::MAX, 2), - "111111111111111111111111111111111111111111111111111111111111111" - ); - assert_eq!( - &int_to_base(i64::MIN, 2), - "-1000000000000000000000000000000000000000000000000000000000000000" - ); - assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807"); - assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808"); - assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff"); - assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000"); - assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7"); - assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8"); - } -} diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs index 222b14d3..dadf0bed 100644 --- a/crates/typst-library/src/compute/data.rs +++ b/crates/typst-library/src/compute/data.rs @@ -4,15 +4,25 @@ use typst::syntax::is_newline; use crate::prelude::*; +/// Hook up all data loading definitions. +pub(super) fn define(global: &mut Scope) { + global.category("data-loading"); + global.define_func::<read>(); + global.define_func::<csv>(); + global.define_func::<json>(); + global.define_func::<toml>(); + global.define_func::<yaml>(); + global.define_func::<cbor>(); + global.define_func::<xml>(); +} + /// Reads plain text or data from a file. /// -/// By default, the file will be read as UTF-8 and returned as a -/// [string]($type/string). +/// By default, the file will be read as UTF-8 and returned as a [string]($str). /// -/// If you specify `{encoding: none}`, this returns raw [bytes]($type/bytes) -/// instead. +/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead. /// -/// ## Example { #example } +/// # Example /// ```example /// An example for a HTML file: \ /// #let text = read("data.html") @@ -21,11 +31,10 @@ use crate::prelude::*; /// Raw bytes: /// #read("tiger.jpg", encoding: none) /// ``` -/// -/// Display: Read -/// Category: data-loading #[func] pub fn read( + /// The virtual machine. + vm: &mut Vm, /// Path to a file. path: Spanned<EcoString>, /// The encoding to read the file with. @@ -34,8 +43,6 @@ pub fn read( #[named] #[default(Some(Encoding::Utf8))] encoding: Option<Encoding>, - /// The virtual machine. - vm: &mut Vm, ) -> SourceResult<Readable> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; @@ -101,7 +108,7 @@ impl From<Readable> for Bytes { /// rows will be collected into a single array. Header rows will not be /// stripped. /// -/// ## Example { #example } +/// # Example /// ```example /// #let results = csv("data.csv") /// @@ -111,15 +118,10 @@ impl From<Readable> for Bytes { /// ..results.flatten(), /// ) /// ``` -/// -/// Display: CSV -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", csv_decode_func()); - scope -)] +#[func(scope, title = "CSV")] pub fn csv( + /// The virtual machine. + vm: &mut Vm, /// Path to a CSV file. path: Spanned<EcoString>, /// The delimiter that separates columns in the CSV file. @@ -127,47 +129,45 @@ pub fn csv( #[named] #[default] delimiter: Delimiter, - /// The virtual machine. - vm: &mut Vm, ) -> SourceResult<Array> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - csv_decode(Spanned::new(Readable::Bytes(data), span), delimiter) + self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter) } -/// Reads structured data from a CSV string/bytes. -/// -/// Display: Decode CSV -/// Category: data-loading -#[func] -pub fn csv_decode( - /// CSV data. - data: Spanned<Readable>, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, -) -> SourceResult<Array> { - let Spanned { v: data, span } = data; - let mut builder = csv::ReaderBuilder::new(); - builder.has_headers(false); - builder.delimiter(delimiter.0 as u8); - let mut reader = builder.from_reader(data.as_slice()); - let mut array = Array::new(); - - for (line, result) in reader.records().enumerate() { - // Original solution use line from error, but that is incorrect with - // `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + 1; // Counting lines from 1 - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let sub = row.into_iter().map(|field| field.into_value()).collect(); - array.push(Value::Array(sub)) - } +#[scope] +impl csv { + /// Reads structured data from a CSV string/bytes. + #[func(title = "Decode CSV")] + pub fn decode( + /// CSV data. + data: Spanned<Readable>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, + ) -> SourceResult<Array> { + let Spanned { v: data, span } = data; + let mut builder = ::csv::ReaderBuilder::new(); + builder.has_headers(false); + builder.delimiter(delimiter.0 as u8); + let mut reader = builder.from_reader(data.as_slice()); + let mut array = Array::new(); + + for (line, result) in reader.records().enumerate() { + // Original solution use line from error, but that is incorrect with + // `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + 1; // Counting lines from 1 + let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; + let sub = row.into_iter().map(|field| field.into_value()).collect(); + array.push(Value::Array(sub)) + } - Ok(array) + Ok(array) + } } /// The delimiter to use when parsing CSV files. @@ -198,10 +198,10 @@ cast! { } /// Format the user-facing CSV error message. -fn format_csv_error(err: csv::Error, line: usize) -> EcoString { +fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { match err.kind() { - csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), + ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { eco_format!( "failed to parse CSV (found {len} instead of \ {expected_len} fields in line {line})" @@ -224,7 +224,7 @@ fn format_csv_error(err: csv::Error, line: usize) -> EcoString { /// The JSON files in the example contain objects with the keys `temperature`, /// `unit`, and `weather`. /// -/// ## Example { #example } +/// # Example /// ```example /// #let forecast(day) = block[ /// #box(square( @@ -248,64 +248,53 @@ fn format_csv_error(err: csv::Error, line: usize) -> EcoString { /// #forecast(json("monday.json")) /// #forecast(json("tuesday.json")) /// ``` -/// -/// Display: JSON -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", json_decode_func()); - scope.define("encode", json_encode_func()); - scope -)] +#[func(scope, title = "JSON")] pub fn json( - /// Path to a JSON file. - path: Spanned<EcoString>, /// The virtual machine. vm: &mut Vm, + /// Path to a JSON file. + path: Spanned<EcoString>, ) -> SourceResult<Value> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - json_decode(Spanned::new(Readable::Bytes(data), span)) + json::decode(Spanned::new(Readable::Bytes(data), span)) } -/// Reads structured data from a JSON string/bytes. -/// -/// Display: JSON -/// Category: data-loading -#[func] -pub fn json_decode( - /// JSON data. - data: Spanned<Readable>, -) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) -} +#[scope] +impl json { + /// Reads structured data from a JSON string/bytes. + #[func(title = "Decode JSON")] + pub fn decode( + /// JSON data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(span) + } -/// Encodes structured data into a JSON string. -/// -/// Display: Encode JSON -/// Category: data-loading -#[func] -pub fn json_encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty print the JSON with newlines and indentation. - #[named] - #[default(true)] - pretty: bool, -) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { - serde_json::to_string_pretty(&value) - } else { - serde_json::to_string(&value) + /// Encodes structured data into a JSON string. + #[func(title = "Encode JSON")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty print the JSON with newlines and indentation. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { + serde_json::to_string_pretty(&value) + } else { + serde_json::to_string(&value) + } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) + .at(span) } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) - .at(span) } /// Reads structured data from a TOML file. @@ -319,7 +308,7 @@ pub fn json_encode( /// The TOML file in the example consists of a table with the keys `title`, /// `version`, and `authors`. /// -/// ## Example { #example } +/// # Example /// ```example /// #let details = toml("details.toml") /// @@ -328,67 +317,56 @@ pub fn json_encode( /// Authors: #(details.authors /// .join(", ", last: " and ")) /// ``` -/// -/// Display: TOML -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", toml_decode_func()); - scope.define("encode", toml_encode_func()); - scope -)] +#[func(scope, title = "TOML")] pub fn toml( - /// Path to a TOML file. - path: Spanned<EcoString>, /// The virtual machine. vm: &mut Vm, + /// Path to a TOML file. + path: Spanned<EcoString>, ) -> SourceResult<Value> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - toml_decode(Spanned::new(Readable::Bytes(data), span)) + toml::decode(Spanned::new(Readable::Bytes(data), span)) } -/// Reads structured data from a TOML string/bytes. -/// -/// Display: Decode TOML -/// Category: data-loading -#[func] -pub fn toml_decode( - /// TOML data. - data: Spanned<Readable>, -) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) -} +#[scope] +impl toml { + /// Reads structured data from a TOML string/bytes. + #[func(title = "Decode TOML")] + pub fn decode( + /// TOML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let raw = std::str::from_utf8(data.as_slice()) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(span) + } -/// Encodes structured data into a TOML string. -/// -/// Display: Encode TOML -/// Category: data-loading -#[func] -pub fn toml_encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty-print the resulting TOML. - #[named] - #[default(true)] - pretty: bool, -) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { toml::to_string_pretty(&value) } else { toml::to_string(&value) } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) - .at(span) + /// Encodes structured data into a TOML string. + #[func(title = "Encode TOML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty-print the resulting TOML. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) + .at(span) + } } /// Format the user-facing TOML error message. -fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString { +fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { let line = head.lines().count(); let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); @@ -415,7 +393,7 @@ fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString { /// each with a sequence of their own submapping with the keys /// "title" and "published" /// -/// ## Example { #example } +/// # Example /// ```example /// #let bookshelf(contents) = { /// for (author, works) in contents { @@ -430,56 +408,45 @@ fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString { /// yaml("scifi-authors.yaml") /// ) /// ``` -/// -/// Display: YAML -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", yaml_decode_func()); - scope.define("encode", yaml_encode_func()); - scope -)] +#[func(scope, title = "YAML")] pub fn yaml( - /// Path to a YAML file. - path: Spanned<EcoString>, /// The virtual machine. vm: &mut Vm, + /// Path to a YAML file. + path: Spanned<EcoString>, ) -> SourceResult<Value> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - yaml_decode(Spanned::new(Readable::Bytes(data), span)) + yaml::decode(Spanned::new(Readable::Bytes(data), span)) } -/// Reads structured data from a YAML string/bytes. -/// -/// Display: Decode YAML -/// Category: data-loading -#[func] -pub fn yaml_decode( - /// YAML data. - data: Spanned<Readable>, -) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) -} +#[scope] +impl yaml { + /// Reads structured data from a YAML string/bytes. + #[func(title = "Decode YAML")] + pub fn decode( + /// YAML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(span) + } -/// Encode structured data into a YAML string. -/// -/// Display: Encode YAML -/// Category: data-loading -#[func] -pub fn yaml_encode( - /// Value to be encoded. - value: Spanned<Value>, -) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - serde_yaml::to_string(&value) - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) - .at(span) + /// Encode structured data into a YAML string. + #[func(title = "Encode YAML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + serde_yaml::to_string(&value) + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) + .at(span) + } } /// Reads structured data from a CBOR file. @@ -490,57 +457,46 @@ pub fn yaml_encode( /// equivalents, null-values (`null`, `~` or empty ``) will be converted into /// `{none}`, and numbers will be converted to floats or integers depending on /// whether they are whole numbers. -/// -/// Display: CBOR -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", cbor_decode_func()); - scope.define("encode", cbor_encode_func()); - scope -)] +#[func(scope, title = "CBOR")] pub fn cbor( - /// Path to a CBOR file. - path: Spanned<EcoString>, /// The virtual machine. vm: &mut Vm, + /// Path to a CBOR file. + path: Spanned<EcoString>, ) -> SourceResult<Value> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - cbor_decode(Spanned::new(data, span)) + cbor::decode(Spanned::new(data, span)) } -/// Reads structured data from CBOR bytes. -/// -/// Display: Decode CBOR -/// Category: data-loading -#[func] -pub fn cbor_decode( - /// cbor data. - data: Spanned<Bytes>, -) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) -} +#[scope] +impl cbor { + /// Reads structured data from CBOR bytes. + #[func(title = "Decode CBOR")] + pub fn decode( + /// cbor data. + data: Spanned<Bytes>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(span) + } -/// Encode structured data into CBOR bytes. -/// -/// Display: Encode CBOR -/// Category: data-loading -#[func] -pub fn cbor_encode( - /// Value to be encoded. - value: Spanned<Value>, -) -> SourceResult<Bytes> { - let Spanned { v: value, span } = value; - let mut res = Vec::new(); - ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) - .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) - .at(span) + /// Encode structured data into CBOR bytes. + #[func(title = "Encode CBOR")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Bytes> { + let Spanned { v: value, span } = value; + let mut res = Vec::new(); + ciborium::into_writer(&value, &mut res) + .map(|_| res.into()) + .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) + .at(span) + } } /// Reads structured data from an XML file. @@ -558,7 +514,7 @@ pub fn cbor_encode( /// `content` tag contains one or more paragraphs, which are represented as `p` /// tags. /// -/// ## Example { #example } +/// # Example /// ```example /// #let find-child(elem, tag) = { /// elem.children @@ -591,41 +547,35 @@ pub fn cbor_encode( /// } /// } /// ``` -/// -/// Display: XML -/// Category: data-loading -#[func] -#[scope( - scope.define("decode", xml_decode_func()); - scope -)] +#[func(scope, title = "XML")] pub fn xml( - /// Path to an XML file. - path: Spanned<EcoString>, /// The virtual machine. vm: &mut Vm, + /// Path to an XML file. + path: Spanned<EcoString>, ) -> SourceResult<Value> { let Spanned { v: path, span } = path; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; - xml_decode(Spanned::new(Readable::Bytes(data), span)) + xml::decode(Spanned::new(Readable::Bytes(data), span)) } -/// Reads structured data from an XML string/bytes. -/// -/// Display: Decode XML -/// Category: data-loading -#[func] -pub fn xml_decode( - /// XML data. - data: Spanned<Readable>, -) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; - Ok(convert_xml(document.root())) +#[scope] +impl xml { + /// Reads structured data from an XML string/bytes. + #[func(title = "Decode XML")] + pub fn decode( + /// XML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let text = std::str::from_utf8(data.as_slice()) + .map_err(FileError::from) + .at(span)?; + let document = + roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; + Ok(convert_xml(document.root())) + } } /// Convert an XML node to a Typst value. diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs index 3d07a3af..dad05717 100644 --- a/crates/typst-library/src/compute/foundations.rs +++ b/crates/typst-library/src/compute/foundations.rs @@ -1,29 +1,32 @@ -use typst::eval::EvalMode; +use typst::eval::{ + Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, +}; use crate::prelude::*; -/// Determines the type of a value. -/// -/// Returns the name of the value's type. -/// -/// ## Example { #example } -/// ```example -/// #type(12) \ -/// #type(14.7) \ -/// #type("hello") \ -/// #type(none) \ -/// #type([Hi]) \ -/// #type(x => x + 1) -/// ``` -/// -/// Display: Type -/// Category: foundations -#[func] -pub fn type_( - /// The value whose type's to determine. - value: Value, -) -> Str { - value.type_name().into() +/// Hook up all foundational definitions. +pub(super) fn define(global: &mut Scope) { + global.category("foundations"); + global.define_type::<bool>(); + global.define_type::<i64>(); + global.define_type::<f64>(); + global.define_type::<Str>(); + global.define_type::<Bytes>(); + global.define_type::<Content>(); + global.define_type::<Array>(); + global.define_type::<Dict>(); + global.define_type::<Func>(); + global.define_type::<Args>(); + global.define_type::<Type>(); + global.define_type::<Module>(); + global.define_type::<Regex>(); + global.define_type::<Datetime>(); + global.define_type::<Duration>(); + global.define_type::<Plugin>(); + global.define_func::<repr>(); + global.define_func::<panic>(); + global.define_func::<assert>(); + global.define_func::<eval>(); } /// Returns the string representation of a value. @@ -35,17 +38,14 @@ pub fn type_( /// **Note:** This function is for debugging purposes. Its output should not be /// considered stable and may change at any time! /// -/// ## Example { #example } +/// # Example /// ```example /// #none vs #repr(none) \ /// #"hello" vs #repr("hello") \ /// #(1, 2) vs #repr((1, 2)) \ /// #[*Hi*] vs #repr([*Hi*]) /// ``` -/// -/// Display: Representation -/// Category: foundations -#[func] +#[func(title = "Representation")] pub fn repr( /// The value whose string representation to produce. value: Value, @@ -55,16 +55,12 @@ pub fn repr( /// Fails with an error. /// -/// ## Example { #example } +/// # Example /// The code below produces the error `panicked with: "this is wrong"`. /// ```typ /// #panic("this is wrong") /// ``` -/// -/// Display: Panic -/// Category: foundations -/// Keywords: error -#[func] +#[func(keywords = ["error"])] pub fn panic( /// The values to panic with. #[variadic] @@ -89,21 +85,13 @@ pub fn panic( /// produce any output in the document. /// /// If you wish to test equality between two values, see -/// [`assert.eq`]($func/assert.eq) and [`assert.ne`]($func/assert.ne). +/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne). /// -/// ## Example { #example } +/// # Example /// ```typ /// #assert(1 < 2, message: "math broke") /// ``` -/// -/// Display: Assert -/// Category: foundations -#[func] -#[scope( - scope.define("eq", assert_eq_func()); - scope.define("ne", assert_ne_func()); - scope -)] +#[func(scope)] pub fn assert( /// The condition that must be true for the assertion to pass. condition: bool, @@ -121,91 +109,83 @@ pub fn assert( Ok(NoneValue) } -/// Ensures that two values are equal. -/// -/// Fails with an error if the first value is not equal to the second. Does not -/// produce any output in the document. -/// -/// ## Example { #example } -/// ```typ -/// #assert.eq(10, 10) -/// ``` -/// -/// Display: Assert Equals -/// Category: foundations -#[func] -pub fn assert_eq( - /// The first value to compare. - left: Value, - - /// The second value to compare. - right: Value, - - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option<EcoString>, -) -> StrResult<NoneValue> { - if left != right { - if let Some(message) = message { - bail!("equality assertion failed: {message}"); - } else { - bail!("equality assertion failed: value {left:?} was not equal to {right:?}"); +#[scope] +impl assert { + /// Ensures that two values are equal. + /// + /// Fails with an error if the first value is not equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.eq(10, 10) + /// ``` + #[func(title = "Assert Equal")] + pub fn eq( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left != right { + if let Some(message) = message { + bail!("equality assertion failed: {message}"); + } else { + bail!("equality assertion failed: value {left:?} was not equal to {right:?}"); + } } + Ok(NoneValue) } - Ok(NoneValue) -} -/// Ensures that two values are not equal. -/// -/// Fails with an error if the first value is equal to the second. Does not -/// produce any output in the document. -/// -/// ## Example { #example } -/// ```typ -/// #assert.ne(3, 4) -/// ``` -/// -/// Display: Assert Not Equals -/// Category: foundations -#[func] -pub fn assert_ne( - /// The first value to compare. - left: Value, - - /// The second value to compare. - right: Value, - - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option<EcoString>, -) -> StrResult<NoneValue> { - if left == right { - if let Some(message) = message { - bail!("inequality assertion failed: {message}"); - } else { - bail!("inequality assertion failed: value {left:?} was equal to {right:?}"); + /// Ensures that two values are not equal. + /// + /// Fails with an error if the first value is equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.ne(3, 4) + /// ``` + #[func(title = "Assert Not Equal")] + pub fn ne( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left == right { + if let Some(message) = message { + bail!("inequality assertion failed: {message}"); + } else { + bail!( + "inequality assertion failed: value {left:?} was equal to {right:?}" + ); + } } + Ok(NoneValue) } - Ok(NoneValue) } /// Evaluates a string as Typst code. /// /// This function should only be used as a last resort. /// -/// ## Example { #example } +/// # Example /// ```example /// #eval("1 + 1") \ /// #eval("(1, 2, 3, 4)").len() \ /// #eval("*Markup!*", mode: "markup") \ /// ``` -/// -/// Display: Evaluate -/// Category: foundations -#[func] +#[func(title = "Evaluate")] pub fn eval( + /// The virtual machine. + vm: &mut Vm, /// A string of Typst code to evaluate. /// /// The code in the string cannot interact with the file system. @@ -235,8 +215,6 @@ pub fn eval( #[named] #[default] scope: Dict, - /// The virtual machine. - vm: &mut Vm, ) -> SourceResult<Value> { let Spanned { v: text, span } = source; let dict = scope; diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs index ca95f7b7..9e897653 100644 --- a/crates/typst-library/src/compute/mod.rs +++ b/crates/typst-library/src/compute/mod.rs @@ -1,11 +1,10 @@ //! Computational functions. pub mod calc; -mod construct; + mod data; mod foundations; -pub use self::construct::*; pub use self::data::*; pub use self::foundations::*; @@ -13,33 +12,7 @@ use crate::prelude::*; /// Hook up all compute definitions. pub(super) fn define(global: &mut Scope) { - global.define("type", type_func()); - global.define("repr", repr_func()); - global.define("panic", panic_func()); - global.define("assert", assert_func()); - global.define("eval", eval_func()); - global.define("int", int_func()); - global.define("float", float_func()); - global.define("luma", luma_func()); - global.define("rgb", rgb_func()); - global.define("cmyk", cmyk_func()); - global.define("color", color_module()); - global.define("datetime", datetime_func()); - global.define("duration", duration_func()); - global.define("symbol", symbol_func()); - global.define("str", str_func()); - global.define("bytes", bytes_func()); - global.define("label", label_func()); - global.define("regex", regex_func()); - global.define("array", array_func()); - global.define("range", range_func()); - global.define("read", read_func()); - global.define("csv", csv_func()); - global.define("json", json_func()); - global.define("toml", toml_func()); - global.define("yaml", yaml_func()); - global.define("cbor", cbor_func()); - global.define("xml", xml_func()); - global.define("calc", calc::module()); - global.define("plugin", plugin_func()); + self::foundations::define(global); + self::data::define(global); + self::calc::define(global); } diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index 5f7e8bc0..f080f677 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// Aligns content horizontally and vertically. /// -/// ## Example { #example } +/// # Example /// ```example /// #set align(center) /// @@ -11,43 +11,9 @@ use crate::prelude::*; /// Not left nor right, it stands alone \ /// A work of art, a visual throne /// ``` -/// -/// Display: Align -/// Category: layout -#[element(Show)] +#[elem(Show)] pub struct AlignElem { - /// The alignment along both axes. - /// - /// Possible values for horizontal alignments are: - /// - `start` - /// - `end` - /// - `left` - /// - `center` - /// - `right` - /// - /// The `start` and `end` alignments are relative to the current [text - /// direction]($func/text.dir). - /// - /// Possible values for vertical alignments are: - /// - `top` - /// - `horizon` - /// - `bottom` - /// - /// You can use the `axis` method on a single-axis alignment to obtain - /// whether it is `{"horizontal"}` or `{"vertical"}`. You can also use the - /// `inv` method to obtain its inverse alignment. For example, - /// `{top.axis()}` is `{"vertical"}`, while `{top.inv()}` is equal to - /// `{bottom}`. - /// - /// To align along both axes at the same time, add the two alignments using - /// the `+` operator to get a `2d alignment`. For example, `top + right` - /// aligns the content to the top right corner. - /// - /// For 2d alignments, the `x` and `y` fields hold their horizontal and - /// vertical components, respectively. Additionally, you can use the `inv` - /// method to obtain a 2d alignment with both components inverted. For - /// instance, `{(top + right).x}` is `right`, `{(top + right).y}` is `top`, - /// and `{(top + right).inv()}` is equal to `bottom + left`. + /// The [alignment]($alignment) along both axes. /// /// ```example /// #set page(height: 6cm) @@ -61,8 +27,8 @@ pub struct AlignElem { /// ``` #[positional] #[fold] - #[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))] - pub alignment: Axes<Option<GenAlign>>, + #[default] + pub alignment: Align, /// The content to align. #[required] @@ -72,8 +38,6 @@ pub struct AlignElem { impl Show for AlignElem { #[tracing::instrument(name = "AlignElem::show", skip_all)] fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .styled(Self::set_alignment(self.alignment(styles).map(Some)))) + Ok(self.body().styled(Self::set_alignment(self.alignment(styles)))) } } diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs index 6645ba9e..961bedc5 100644 --- a/crates/typst-library/src/layout/columns.rs +++ b/crates/typst-library/src/layout/columns.rs @@ -10,9 +10,9 @@ use crate::text::TextElem; /// necessary. /// /// If you need to insert columns across your whole document, you can use the -/// [`{page}` function's `columns` parameter]($func/page.columns) instead. +/// [`{page}` function's `columns` parameter]($page.columns) instead. /// -/// ## Example { #example } +/// # Example /// ```example /// = Towards Advanced Deep Learning /// @@ -32,10 +32,7 @@ use crate::text::TextElem; /// increasingly been used to solve a /// variety of problems. /// ``` -/// -/// Display: Columns -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct ColumnsElem { /// The number of columns. #[positional] @@ -132,11 +129,11 @@ impl Layout for ColumnsElem { /// Forces a column break. /// -/// The function will behave like a [page break]($func/pagebreak) when used in a +/// The function will behave like a [page break]($pagebreak) when used in a /// single column layout or the last column on a page. Otherwise, content after /// the column break will be placed in the next column. /// -/// ## Example { #example } +/// # Example /// ```example /// #set page(columns: 2) /// Preliminary findings from our @@ -153,10 +150,7 @@ impl Layout for ColumnsElem { /// understanding of the fundamental /// laws of nature. /// ``` -/// -/// Display: Column Break -/// Category: layout -#[element(Behave)] +#[elem(title = "Column Break", Behave)] pub struct ColbreakElem { /// If `{true}`, the column break is skipped if the current column is /// already empty. diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index c79669d0..e966398f 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -11,7 +11,7 @@ use crate::prelude::*; /// elements into a paragraph. Boxes take the size of their contents by default /// but can also be sized explicitly. /// -/// ## Example { #example } +/// # Example /// ```example /// Refer to the docs /// #box( @@ -20,15 +20,12 @@ use crate::prelude::*; /// ) /// for more information. /// ``` -/// -/// Display: Box -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct BoxElem { /// The width of the box. /// - /// Boxes can have [fractional]($type/fraction) widths, as the example - /// below demonstrates. + /// Boxes can have [fractional]($fraction) widths, as the example below + /// demonstrates. /// /// _Note:_ Currently, only boxes and only their widths might be fractionally /// sized within paragraphs. Support for fractionally sized images, shapes, @@ -51,23 +48,29 @@ pub struct BoxElem { pub baseline: Rel<Length>, /// The box's background color. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// [rectangle's documentation]($rect.fill) for more details. pub fill: Option<Paint>, /// The box's border color. See the - /// [rectangle's documentation]($func/rect.stroke) for more details. + /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] - pub stroke: Sides<Option<Option<PartialStroke>>>, + pub stroke: Sides<Option<Option<Stroke>>>, - /// How much to round the box's corners. See the [rectangle's - /// documentation]($func/rect.radius) for more details. + /// How much to round the box's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. #[resolve] #[fold] pub radius: Corners<Option<Rel<Length>>>, - /// How much to pad the box's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. + /// How much to pad the box's content. + /// + /// _Note:_ When the box contains text, its exact size depends on the + /// current [text edges]($text.top-edge). + /// + /// ```example + /// #rect(inset: 0pt)[Tight] + /// ``` #[resolve] #[fold] pub inset: Sides<Option<Rel<Length>>>, @@ -76,7 +79,7 @@ pub struct BoxElem { /// /// This is useful to prevent padding from affecting line layout. For a /// generalized version of the example below, see the documentation for the - /// [raw text's block parameter]($func/raw.block). + /// [raw text's block parameter]($raw.block). /// /// ```example /// An inline @@ -119,8 +122,7 @@ impl Layout for BoxElem { let expand = sizing.as_ref().map(Smart::is_custom); let size = sizing .resolve(styles) - .zip(regions.base()) - .map(|(s, b)| s.map(|v| v.relative_to(b))) + .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) .unwrap_or(regions.base()); // Apply inset. @@ -151,7 +153,7 @@ impl Layout for BoxElem { // Prepare fill and stroke. let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); + let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); // Add fill and/or stroke. if fill.is_some() || stroke.iter().any(Option::is_some) { @@ -172,7 +174,7 @@ impl Layout for BoxElem { /// Such a container can be used to separate content, size it, and give it a /// background or border. /// -/// ## Examples { #examples } +/// # Examples /// With a block, you can give a background to content while still allowing it /// to break across multiple pages. /// ```example @@ -196,10 +198,7 @@ impl Layout for BoxElem { /// = Blocky /// More text. /// ``` -/// -/// Display: Block -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct BlockElem { /// The block's width. /// @@ -215,7 +214,7 @@ pub struct BlockElem { pub width: Smart<Rel<Length>>, /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($func/block.breakable) is `{true}`, the + /// on a page and [`breakable`]($block.breakable) is `{true}`, the /// block will continue on the next page with the remaining height. /// /// ```example @@ -244,29 +243,29 @@ pub struct BlockElem { pub breakable: bool, /// The block's background color. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// [rectangle's documentation]($rect.fill) for more details. pub fill: Option<Paint>, /// The block's border color. See the - /// [rectangle's documentation]($func/rect.stroke) for more details. + /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] - pub stroke: Sides<Option<Option<PartialStroke>>>, + pub stroke: Sides<Option<Option<Stroke>>>, - /// How much to round the block's corners. See the [rectangle's - /// documentation]($func/rect.radius) for more details. + /// How much to round the block's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. #[resolve] #[fold] pub radius: Corners<Option<Rel<Length>>>, - /// How much to pad the block's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. + /// How much to pad the block's content. See the + /// [box's documentation]($box.inset) for more details. #[resolve] #[fold] pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the block's size without affecting the layout. See - /// the [rectangle's documentation]($func/rect.outset) for more details. + /// the [box's documentation]($box.outset) for more details. #[resolve] #[fold] pub outset: Sides<Option<Rel<Length>>>, @@ -352,8 +351,7 @@ impl Layout for BlockElem { let mut expand = sizing.as_ref().map(Smart::is_custom); let mut size = sizing .resolve(styles) - .zip(regions.base()) - .map(|(s, b)| s.map(|v| v.relative_to(b))) + .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) .unwrap_or(regions.base()); // Layout the child. @@ -418,7 +416,7 @@ impl Layout for BlockElem { // Prepare fill and stroke. let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); + let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); // Add fill and/or stroke. if fill.is_some() || stroke.iter().any(Option::is_some) { diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs index 8f23b6dc..c505f189 100644 --- a/crates/typst-library/src/layout/enum.rs +++ b/crates/typst-library/src/layout/enum.rs @@ -11,7 +11,7 @@ use super::GridLayouter; /// /// Displays a sequence of items vertically and numbers them consecutively. /// -/// ## Example { #example } +/// # Example /// ```example /// Automatically numbered: /// + Preparations @@ -41,8 +41,8 @@ use super::GridLayouter; /// + Don't forget step two /// ``` /// -/// You can also use [`enum.item`]($func/enum.item) to programmatically -/// customize the number of each item in the enumeration: +/// You can also use [`enum.item`]($enum.item) to programmatically customize the +/// number of each item in the enumeration: /// /// ```example /// #enum( @@ -52,7 +52,7 @@ use super::GridLayouter; /// ) /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This functions also has dedicated syntax: /// /// - Starting a line with a plus sign creates an automatically numbered @@ -63,18 +63,11 @@ use super::GridLayouter; /// Enumeration items can contain multiple paragraphs and other block-level /// content. All content that is indented more than an item's marker becomes /// part of that item. -/// -/// Display: Numbered List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", EnumItem::func()); - scope -)] +#[elem(scope, title = "Numbered List", Layout)] pub struct EnumElem { /// If this is `{false}`, the items are spaced apart with - /// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the enumeration more + /// [enum spacing]($enum.spacing). If it is `{true}`, they use normal + /// [leading]($par.leading) instead. This makes the enumeration more /// compact, which can look better if the items are short. /// /// In markup mode, the value of this parameter is determined based on @@ -95,7 +88,7 @@ pub struct EnumElem { pub tight: bool, /// How to number the enumeration. Accepts a - /// [numbering pattern or function]($func/numbering). + /// [numbering pattern or function]($numbering). /// /// If the numbering pattern contains multiple counting symbols, they apply /// to nested enums. If given a function, the function receives one argument @@ -153,7 +146,7 @@ pub struct EnumElem { /// The spacing between the items of a wide (non-tight) enumeration. /// - /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($block.below). pub spacing: Smart<Spacing>, /// The horizontal alignment that enum numbers should have. @@ -177,8 +170,8 @@ pub struct EnumElem { /// 16. Sixteen /// 32. Thirty two /// ```` - #[default(HorizontalAlign(GenAlign::End))] - pub number_align: HorizontalAlign, + #[default(HAlign::End)] + pub number_align: HAlign, /// The numbered list's items. /// @@ -201,6 +194,12 @@ pub struct EnumElem { parents: Parent, } +#[scope] +impl EnumElem { + #[elem] + type EnumItem; +} + impl Layout for EnumElem { #[tracing::instrument(name = "EnumElem::layout", skip_all)] fn layout( @@ -225,11 +224,10 @@ impl Layout for EnumElem { let full = self.full(styles); // Horizontally align based on the given respective parameter. - // Vertically align to the top to avoid inheriting 'horizon' or - // 'bottom' alignment from the context and having the number be - // displaced in relation to the item it refers to. - let number_align: Axes<Option<GenAlign>> = - Axes::new(self.number_align(styles).into(), Align::Top.into()).map(Some); + // Vertically align to the top to avoid inheriting `horizon` or `bottom` + // alignment from the context and having the number be displaced in + // relation to the item it refers to. + let number_align = self.number_align(styles) + VAlign::Top; for item in self.children() { number = item.number(styles).unwrap_or(number); @@ -278,10 +276,7 @@ impl Layout for EnumElem { } /// An enumeration item. -/// -/// Display: Numbered List Item -/// Category: layout -#[element] +#[elem(name = "item", title = "Numbered List Item")] pub struct EnumItem { /// The item's number. #[positional] diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs index 4ce78c94..fe6e9398 100644 --- a/crates/typst-library/src/layout/flow.rs +++ b/crates/typst-library/src/layout/flow.rs @@ -14,10 +14,7 @@ use crate::visualize::{ /// /// This element is responsible for layouting both the top-level content flow /// and the contents of boxes. -/// -/// Display: Flow -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct FlowElem { /// The children that will be arranges into a flow. #[variadic] @@ -62,7 +59,7 @@ impl Layout for FlowElem { frame.meta(styles, true); layouter.items.push(FlowItem::Frame { frame, - aligns: Axes::new(Align::Top, Align::Left), + align: Axes::splat(FixedAlign::Start), sticky: true, movable: false, }); @@ -128,12 +125,12 @@ enum FlowItem { /// A frame for a layouted block, how to align it, whether it sticks to the /// item after it (for orphan prevention), and whether it is movable /// (to keep it together with its footnotes). - Frame { frame: Frame, aligns: Axes<Align>, sticky: bool, movable: bool }, + Frame { frame: Frame, align: Axes<FixedAlign>, sticky: bool, movable: bool }, /// An absolutely placed frame. Placed { frame: Frame, - x_align: Align, - y_align: Smart<Option<Align>>, + x_align: FixedAlign, + y_align: Smart<Option<FixedAlign>>, delta: Axes<Rel<Abs>>, float: bool, clearance: Abs, @@ -209,7 +206,7 @@ impl<'a> FlowLayouter<'a> { par: &ParElem, styles: StyleChain, ) -> SourceResult<()> { - let aligns = AlignElem::alignment_in(styles).resolve(styles); + let align = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); let consecutive = self.last_was_par; let lines = par @@ -242,7 +239,7 @@ impl<'a> FlowLayouter<'a> { self.layout_item( vt, - FlowItem::Frame { frame, aligns, sticky: false, movable: true }, + FlowItem::Frame { frame, align, sticky: false, movable: true }, )?; } @@ -258,11 +255,11 @@ impl<'a> FlowLayouter<'a> { content: &dyn Layout, styles: StyleChain, ) -> SourceResult<()> { - let aligns = AlignElem::alignment_in(styles).resolve(styles); + let align = AlignElem::alignment_in(styles).resolve(styles); let sticky = BlockElem::sticky_in(styles); let pod = Regions::one(self.regions.base(), Axes::splat(false)); let frame = content.layout(vt, styles, pod)?.into_frame(); - self.layout_item(vt, FlowItem::Frame { frame, aligns, sticky, movable: true })?; + self.layout_item(vt, FlowItem::Frame { frame, align, sticky, movable: true })?; self.last_was_par = false; Ok(()) } @@ -278,10 +275,10 @@ impl<'a> FlowLayouter<'a> { let clearance = placed.clearance(styles); let alignment = placed.alignment(styles); let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); - let x_align = alignment.map_or(Align::Center, |aligns| { - aligns.x.unwrap_or(GenAlign::Start).resolve(styles) + let x_align = alignment.map_or(FixedAlign::Center, |align| { + align.x().unwrap_or_default().resolve(styles) }); - let y_align = alignment.map(|align| align.y.resolve(styles)); + let y_align = alignment.map(|align| align.y().map(VAlign::fix)); let frame = placed.layout(vt, styles, self.regions)?.into_frame(); let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; self.layout_item(vt, item) @@ -309,7 +306,7 @@ impl<'a> FlowLayouter<'a> { } // How to align the block. - let aligns = if let Some(align) = block.to::<AlignElem>() { + let align = if let Some(align) = block.to::<AlignElem>() { align.alignment(styles) } else if let Some((_, local)) = block.to_styled() { AlignElem::alignment_in(styles.chain(local)) @@ -332,7 +329,7 @@ impl<'a> FlowLayouter<'a> { self.finish_region(vt)?; } - let item = FlowItem::Frame { frame, aligns, sticky, movable: false }; + let item = FlowItem::Frame { frame, align, sticky, movable: false }; self.layout_item(vt, item)?; } @@ -404,14 +401,14 @@ impl<'a> FlowLayouter<'a> { - (frame.height() + clearance) / 2.0) / self.regions.full; let better_align = - if ratio <= 0.5 { Align::Bottom } else { Align::Top }; + if ratio <= 0.5 { FixedAlign::End } else { FixedAlign::Start }; *y_align = Smart::Custom(Some(better_align)); } // Add some clearance so that the float doesn't touch the main // content. frame.size_mut().y += clearance; - if *y_align == Smart::Custom(Some(Align::Bottom)) { + if *y_align == Smart::Custom(Some(FixedAlign::End)) { frame.translate(Point::with_y(clearance)); } @@ -459,8 +456,10 @@ impl<'a> FlowLayouter<'a> { } FlowItem::Placed { float: false, .. } => {} FlowItem::Placed { frame, float: true, y_align, .. } => match y_align { - Smart::Custom(Some(Align::Top)) => float_top_height += frame.height(), - Smart::Custom(Some(Align::Bottom)) => { + Smart::Custom(Some(FixedAlign::Start)) => { + float_top_height += frame.height() + } + Smart::Custom(Some(FixedAlign::End)) => { float_bottom_height += frame.height() } _ => {} @@ -486,7 +485,7 @@ impl<'a> FlowLayouter<'a> { } let mut output = Frame::new(size); - let mut ruler = Align::Top; + let mut ruler = FixedAlign::Start; let mut float_top_offset = Abs::zero(); let mut offset = float_top_height; let mut float_bottom_offset = Abs::zero(); @@ -502,9 +501,9 @@ impl<'a> FlowLayouter<'a> { let remaining = self.initial.y - used.y; offset += v.share(fr, remaining); } - FlowItem::Frame { frame, aligns, .. } => { - ruler = ruler.max(aligns.y); - let x = aligns.x.position(size.x - frame.width()); + FlowItem::Frame { frame, align, .. } => { + ruler = ruler.max(align.y); + let x = align.x.position(size.x - frame.width()); let y = offset + ruler.position(size.y - used.y); let pos = Point::new(x, y); offset += frame.height(); @@ -514,12 +513,12 @@ impl<'a> FlowLayouter<'a> { let x = x_align.position(size.x - frame.width()); let y = if float { match y_align { - Smart::Custom(Some(Align::Top)) => { + Smart::Custom(Some(FixedAlign::Start)) => { let y = float_top_offset; float_top_offset += frame.height(); y } - Smart::Custom(Some(Align::Bottom)) => { + Smart::Custom(Some(FixedAlign::End)) => { let y = size.y - footnote_height - float_bottom_height + float_bottom_offset; float_bottom_offset += frame.height(); @@ -537,7 +536,7 @@ impl<'a> FlowLayouter<'a> { }; let pos = Point::new(x, y) - + delta.zip(size).map(|(d, s)| d.relative_to(s)).to_point(); + + delta.zip_map(size, Rel::relative_to).to_point(); output.push_frame(pos, frame); } diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs index 4f5175e9..06962524 100644 --- a/crates/typst-library/src/layout/grid.rs +++ b/crates/typst-library/src/layout/grid.rs @@ -34,7 +34,7 @@ use super::Sizing; /// instead of an array. For example, `columns:` `{3}` is equivalent to /// `columns:` `{(auto, auto, auto)}`. /// -/// ## Example { #example } +/// # Example /// ```example /// #set text(10pt, style: "italic") /// #let cell = rect.with( @@ -58,10 +58,7 @@ use super::Sizing; /// cell[One more thing...], /// ) /// ``` -/// -/// Display: Grid -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct GridElem { /// The column sizes. /// diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index c6e83e0c..7f17a7d7 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -7,15 +7,12 @@ use crate::prelude::*; /// content. It may also be useful to redact content because its arguments are /// not included in the output. /// -/// ## Example { #example } +/// # Example /// ```example /// Hello Jane \ /// #hide[Hello] Joe /// ``` -/// -/// Display: Hide -/// Category: layout -#[element(Show)] +#[elem(Show)] pub struct HideElem { /// The content to hide. #[required] diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs index 8bb8744b..a9dad85b 100644 --- a/crates/typst-library/src/layout/list.rs +++ b/crates/typst-library/src/layout/list.rs @@ -9,7 +9,7 @@ use super::GridLayouter; /// Displays a sequence of items vertically, with each item introduced by a /// marker. /// -/// ## Example { #example } +/// # Example /// ```example /// Normal list. /// - Text @@ -30,24 +30,17 @@ use super::GridLayouter; /// ) /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This functions also has dedicated syntax: Start a line with a hyphen, /// followed by a space to create a list item. A list item can contain multiple /// paragraphs and other block-level content. All content that is indented /// more than an item's marker becomes part of that item. -/// -/// Display: Bullet List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", ListItem::func()); - scope -)] +#[elem(scope, title = "Bullet List", Layout)] pub struct ListElem { - /// If this is `{false}`, the items are spaced apart with [list - /// spacing]($func/list.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the list more compact, - /// which can look better if the items are short. + /// If this is `{false}`, the items are spaced apart with + /// [list spacing]($list.spacing). If it is `{true}`, they use normal + /// [leading]($par.leading) instead. This makes the list more compact, which + /// can look better if the items are short. /// /// In markup mode, the value of this parameter is determined based on /// whether items are separated with a blank line. If items directly follow @@ -98,7 +91,7 @@ pub struct ListElem { /// The spacing between the items of a wide (non-tight) list. /// - /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($block.below). pub spacing: Smart<Spacing>, /// The bullet list's children. @@ -120,6 +113,12 @@ pub struct ListElem { depth: Depth, } +#[scope] +impl ListElem { + #[elem] + type ListItem; +} + impl Layout for ListElem { #[tracing::instrument(name = "ListElem::layout", skip_all)] fn layout( @@ -142,7 +141,7 @@ impl Layout for ListElem { .marker(styles) .resolve(vt, depth)? // avoid '#set align' interference with the list - .aligned(Align::LEFT_TOP.into()); + .aligned(HAlign::Start + VAlign::Top); let mut cells = vec![]; for item in self.children() { @@ -170,10 +169,7 @@ impl Layout for ListElem { } /// A bullet list item. -/// -/// Display: Bullet List Item -/// Category: layout -#[element] +#[elem(name = "item", title = "Bullet List Item")] pub struct ListItem { /// The item's body. #[required] diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index eb8e509e..d41b7f95 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -2,13 +2,13 @@ use crate::prelude::*; /// Measures the layouted size of content. /// -/// The `measure` function lets you determine the layouted size of content. -/// Note that an infinite space is assumed, therefore the measured height/width -/// may not necessarily match the final height/width of the measured content. -/// If you want to measure in the current layout dimensions, you can combined -/// `measure` and [`layout`]($func/layout). +/// The `measure` function lets you determine the layouted size of content. Note +/// that an infinite space is assumed, therefore the measured height/width may +/// not necessarily match the final height/width of the measured content. If you +/// want to measure in the current layout dimensions, you can combine `measure` +/// and [`layout`]($layout). /// -/// # Example { #example } +/// # Example /// The same content can have a different size depending on the styles that /// are active when it is layouted. For example, in the example below /// `[#content]` is of course bigger when we increase the font size. @@ -21,8 +21,8 @@ use crate::prelude::*; /// ``` /// /// To do a meaningful measurement, you therefore first need to retrieve the -/// active styles with the [`style`]($func/style) function. You can then pass -/// them to the `measure` function. +/// active styles with the [`style`]($style) function. You can then pass them to +/// the `measure` function. /// /// ```example /// #let thing(body) = style(styles => { @@ -35,18 +35,15 @@ use crate::prelude::*; /// ``` /// /// The measure function returns a dictionary with the entries `width` and -/// `height`, both of type [`length`]($type/length). -/// -/// Display: Measure -/// Category: layout +/// `height`, both of type [`length`]($length). #[func] pub fn measure( + /// The virtual machine. + vm: &mut Vm, /// The content whose size to measure. content: Content, /// The styles with which to layout the content. styles: Styles, - /// The virtual machine. - vm: &mut Vm, ) -> SourceResult<Dict> { let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); let styles = StyleChain::new(&styles); diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index 3334d5aa..ace5cd6e 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -10,7 +10,8 @@ mod fragment; mod grid; mod hide; mod list; -mod measure; +#[path = "measure.rs"] +mod measure_; mod pad; mod page; mod par; @@ -32,7 +33,7 @@ pub use self::fragment::*; pub use self::grid::*; pub use self::hide::*; pub use self::list::*; -pub use self::measure::*; +pub use self::measure_::*; pub use self::pad::*; pub use self::page::*; pub use self::par::*; @@ -57,7 +58,7 @@ use crate::math::{EquationElem, LayoutMath}; use crate::meta::DocumentElem; use crate::prelude::*; use crate::shared::BehavedBuilder; -use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; +use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem}; use crate::visualize::{ CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, SquareElem, @@ -65,43 +66,39 @@ use crate::visualize::{ /// Hook up all layout definitions. pub(super) fn define(global: &mut Scope) { - global.define("page", PageElem::func()); - global.define("pagebreak", PagebreakElem::func()); - global.define("v", VElem::func()); - global.define("par", ParElem::func()); - global.define("parbreak", ParbreakElem::func()); - global.define("h", HElem::func()); - global.define("box", BoxElem::func()); - global.define("block", BlockElem::func()); - global.define("list", ListElem::func()); - global.define("enum", EnumElem::func()); - global.define("terms", TermsElem::func()); - global.define("table", TableElem::func()); - global.define("stack", StackElem::func()); - global.define("grid", GridElem::func()); - global.define("columns", ColumnsElem::func()); - global.define("colbreak", ColbreakElem::func()); - global.define("place", PlaceElem::func()); - global.define("align", AlignElem::func()); - global.define("pad", PadElem::func()); - global.define("repeat", RepeatElem::func()); - global.define("move", MoveElem::func()); - global.define("scale", ScaleElem::func()); - global.define("rotate", RotateElem::func()); - global.define("hide", HideElem::func()); - global.define("measure", measure_func()); - global.define("ltr", Dir::LTR); - global.define("rtl", Dir::RTL); - global.define("ttb", Dir::TTB); - global.define("btt", Dir::BTT); - global.define("start", GenAlign::Start); - global.define("end", GenAlign::End); - global.define("left", GenAlign::Specific(Align::Left)); - global.define("center", GenAlign::Specific(Align::Center)); - global.define("right", GenAlign::Specific(Align::Right)); - global.define("top", GenAlign::Specific(Align::Top)); - global.define("horizon", GenAlign::Specific(Align::Horizon)); - global.define("bottom", GenAlign::Specific(Align::Bottom)); + global.category("layout"); + global.define_type::<Length>(); + global.define_type::<Angle>(); + global.define_type::<Ratio>(); + global.define_type::<Rel<Length>>(); + global.define_type::<Fr>(); + global.define_type::<Dir>(); + global.define_type::<Align>(); + global.define_elem::<PageElem>(); + global.define_elem::<PagebreakElem>(); + global.define_elem::<VElem>(); + global.define_elem::<ParElem>(); + global.define_elem::<ParbreakElem>(); + global.define_elem::<HElem>(); + global.define_elem::<BoxElem>(); + global.define_elem::<BlockElem>(); + global.define_elem::<ListElem>(); + global.define_elem::<EnumElem>(); + global.define_elem::<TermsElem>(); + global.define_elem::<TableElem>(); + global.define_elem::<StackElem>(); + global.define_elem::<GridElem>(); + global.define_elem::<ColumnsElem>(); + global.define_elem::<ColbreakElem>(); + global.define_elem::<PlaceElem>(); + global.define_elem::<AlignElem>(); + global.define_elem::<PadElem>(); + global.define_elem::<RepeatElem>(); + global.define_elem::<MoveElem>(); + global.define_elem::<ScaleElem>(); + global.define_elem::<RotateElem>(); + global.define_elem::<HideElem>(); + global.define_func::<measure>(); } /// Root-level layout. @@ -598,7 +595,7 @@ impl<'a> ParBuilder<'a> { || content.is::<TextElem>() || content.is::<HElem>() || content.is::<LinebreakElem>() - || content.is::<SmartQuoteElem>() + || content.is::<SmartquoteElem>() || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles)) || content.is::<BoxElem>() { diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs index a3d5646b..d1b0cb1f 100644 --- a/crates/typst-library/src/layout/pad.rs +++ b/crates/typst-library/src/layout/pad.rs @@ -5,7 +5,7 @@ use crate::prelude::*; /// The spacing can be specified for each side individually, or for all sides at /// once by specifying a positional argument. /// -/// ## Example { #example } +/// # Example /// ```example /// #set align(center) /// @@ -13,10 +13,7 @@ use crate::prelude::*; /// _Typing speeds can be /// measured in words per minute._ /// ``` -/// -/// Display: Padding -/// Category: layout -#[element(Layout)] +#[elem(title = "Padding", Layout)] pub struct PadElem { /// The padding at the left side. #[parse( @@ -120,6 +117,5 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size { /// <=> (1 - p.rel) * w = s + p.abs /// <=> w = (s + p.abs) / (1 - p.rel) fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size { - size.zip(padding.sum_by_axis()) - .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) + size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs).safe_div(1.0 - p.rel.get())) } diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 4ef90753..d182a417 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -1,6 +1,8 @@ use std::ptr; use std::str::FromStr; +use typst::eval::AutoValue; + use super::{AlignElem, ColumnsElem}; use crate::meta::{Counter, CounterKey, Numbering}; use crate::prelude::*; @@ -18,17 +20,14 @@ use crate::text::TextElem; /// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use /// this and related functions to set up a document with many examples. /// -/// ## Example { #example } +/// # Example /// ```example /// >>> #set page(margin: auto) /// #set page("us-letter") /// /// There you go, US friends! /// ``` -/// -/// Display: Page -/// Category: layout -#[element] +#[elem] pub struct PageElem { /// A standard paper size to set width and height. #[external] @@ -59,9 +58,9 @@ pub struct PageElem { /// The height of the page. /// /// If this is set to `{auto}`, page breaks can only be triggered manually - /// by inserting a [page break]($func/pagebreak). Most examples throughout - /// this documentation use `{auto}` for the height of the page to - /// dynamically grow and shrink to fit their content. + /// by inserting a [page break]($pagebreak). Most examples throughout this + /// documentation use `{auto}` for the height of the page to dynamically + /// grow and shrink to fit their content. #[resolve] #[parse( args.named("height")? @@ -103,9 +102,9 @@ pub struct PageElem { /// - `bottom`: The bottom margin. /// - `left`: The left margin. /// - `inside`: The margin at the inner side of the page (where the - /// [binding]($func/page.binding) is). + /// [binding]($page.binding) is). /// - `outside`: The margin at the outer side of the page (opposite to the - /// [binding]($func/page.binding)). + /// [binding]($page.binding)). /// - `x`: The horizontal margins. /// - `y`: The vertical margins. /// - `rest`: The margins on all sides except those for which the @@ -132,7 +131,7 @@ pub struct PageElem { /// On which side the pages will be bound. /// - /// - `{auto}`: Equivalent to `left` if the [text direction]($func/text.dir) + /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir) /// is left-to-right and `right` if it is right-to-left. /// - `left`: Bound on the left side. /// - `right`: Bound on the right side. @@ -144,7 +143,7 @@ pub struct PageElem { /// How many columns the page has. /// /// If you need to insert columns into a page or other container, you can - /// also use the [`columns` function]($func/columns). + /// also use the [`columns` function]($columns). /// /// ```example:single /// #set page(columns: 2, height: 4.8cm) @@ -175,7 +174,7 @@ pub struct PageElem { /// ``` pub fill: Option<Paint>, - /// How to [number]($func/numbering) the pages. + /// How to [number]($numbering) the pages. /// /// If an explicit `footer` (or `header` for top-aligned numbering) is /// given, the numbering is ignored. @@ -207,17 +206,17 @@ pub struct PageElem { /// /// #lorem(30) /// ``` - #[default(Align::Center.into())] + #[default(HAlign::Center + VAlign::Bottom)] #[parse({ - let spanned: Option<Spanned<Axes<_>>> = args.named("number-align")?; - if let Some(Spanned { v, span }) = spanned { - if matches!(v.y, Some(GenAlign::Specific(Align::Horizon))) { + let option: Option<Spanned<Align>> = args.named("number-align")?; + if let Some(Spanned { v: align, span }) = option { + if align.y() == Some(VAlign::Horizon) { bail!(span, "page number cannot be `horizon`-aligned"); } } - spanned.map(|s| s.v) + option.map(|spanned| spanned.v) })] - pub number_align: Axes<Option<GenAlign>>, + pub number_align: Align, /// The page's header. Fills the top margin of each page. /// @@ -245,7 +244,7 @@ pub struct PageElem { /// /// For just a page number, the `numbering` property, typically suffices. If /// you want to create a custom footer, but still display the page number, - /// you can directly access the [page counter]($func/counter). + /// you can directly access the [page counter]($counter). /// /// ```example /// #set par(justify: true) @@ -406,14 +405,14 @@ impl PageElem { // We interpret the Y alignment as selecting header or footer // and then ignore it for aligning the actual number. - if let Some(x) = number_align.x { - counter = counter.aligned(Axes::with_x(Some(x))); + if let Some(x) = number_align.x() { + counter = counter.aligned(x.into()); } counter }); - if matches!(number_align.y, Some(GenAlign::Specific(Align::Top))) { + if matches!(number_align.y(), Some(VAlign::Top)) { header = header.or(numbering_marginal); } else { footer = footer.or(numbering_marginal); @@ -461,16 +460,16 @@ impl PageElem { let ascent = header_ascent.relative_to(margin.top); pos = Point::with_x(margin.left); area = Size::new(pw, margin.top - ascent); - align = Align::Bottom.into(); + align = Align::BOTTOM; } else if ptr::eq(marginal, &footer) { let descent = footer_descent.relative_to(margin.bottom); pos = Point::new(margin.left, size.y - margin.bottom + descent); area = Size::new(pw, margin.bottom - descent); - align = Align::Top.into(); + align = Align::TOP; } else { pos = Point::zero(); area = size; - align = Align::CENTER_HORIZON.into(); + align = HAlign::Center + VAlign::Horizon; }; let pod = Regions::one(area, Axes::splat(true)); @@ -626,12 +625,12 @@ impl Binding { cast! { Binding, self => match self { - Self::Left => GenAlign::Specific(Align::Left).into_value(), - Self::Right => GenAlign::Specific(Align::Right).into_value(), + Self::Left => Align::LEFT.into_value(), + Self::Right => Align::RIGHT.into_value(), }, - v: GenAlign => match v { - GenAlign::Specific(Align::Left) => Self::Left, - GenAlign::Specific(Align::Right) => Self::Right, + v: Align => match v { + Align::LEFT => Self::Left, + Align::RIGHT => Self::Right, _ => bail!("must be `left` or `right`"), }, } @@ -669,7 +668,7 @@ cast! { /// /// Must not be used inside any containers. /// -/// ## Example { #example } +/// # Example /// ```example /// The next page contains /// more details on compound theory. @@ -678,10 +677,7 @@ cast! { /// == Compound Theory /// In 1984, the first ... /// ``` -/// -/// Display: Page Break -/// Category: layout -#[element] +#[elem(title = "Page Break")] pub struct PagebreakElem { /// If `{true}`, the page break is skipped if the current page is already /// empty. diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs index 39689477..e28e661c 100644 --- a/crates/typst-library/src/layout/par.rs +++ b/crates/typst-library/src/layout/par.rs @@ -16,7 +16,7 @@ use crate::layout::AlignElem; use crate::math::EquationElem; use crate::prelude::*; use crate::text::{ - is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem, + is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartquoteElem, SpaceElem, TextElem, }; @@ -26,7 +26,7 @@ use crate::text::{ /// properties, it can also be used to explicitly render its argument onto a /// paragraph of its own. /// -/// ## Example { #example } +/// # Example /// ```example /// #show par: set block(spacing: 0.65em) /// #set par( @@ -45,10 +45,7 @@ use crate::text::{ /// let $a$ be the smallest of the /// three integers. Then, we ... /// ``` -/// -/// Display: Paragraph -/// Category: layout -#[element(Construct)] +#[elem(title = "Paragraph", Construct)] pub struct ParElem { /// The spacing between lines. #[resolve] @@ -57,13 +54,13 @@ pub struct ParElem { /// Whether to justify text in its line. /// - /// Hyphenation will be enabled for justified paragraphs if the [text - /// property hyphenate]($func/text.hyphenate) is set to `{auto}` and the - /// current language is known. + /// Hyphenation will be enabled for justified paragraphs if the + /// [text function's `hyphenate` property]($text.hyphenate) is set to + /// `{auto}` and the current language is known. /// - /// Note that the current [alignment]($func/align) still has an effect on - /// the placement of the last line except if it ends with a [justified line - /// break]($func/linebreak.justify). + /// Note that the current [alignment]($align) still has an effect on the + /// placement of the last line except if it ends with a + /// [justified line break]($linebreak.justify). #[default(false)] pub justify: bool, @@ -88,7 +85,6 @@ pub struct ParElem { /// challenging to break in a visually /// pleasing way. /// ``` - #[default] pub linebreaks: Smart<Linebreaks>, /// The indent the first line of a paragraph should have. @@ -98,7 +94,7 @@ pub struct ParElem { /// /// By typographic convention, paragraph breaks are indicated either by some /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($func/block.spacing) to the [`leading`] when + /// the [paragraph spacing]($block.spacing) to the [`leading`] when /// using this property (e.g. using /// `[#show par: set block(spacing: 0.65em)]`). pub first_line_indent: Length, @@ -219,7 +215,7 @@ pub enum Linebreaks { /// [for loops]($scripting/#loops). Multiple consecutive /// paragraph breaks collapse into a single one. /// -/// ## Example { #example } +/// # Example /// ```example /// #for i in range(3) { /// [Blind text #i: ] @@ -228,13 +224,10 @@ pub enum Linebreaks { /// } /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// Instead of calling this function, you can insert a blank line into your /// markup to create a paragraph break. -/// -/// Display: Paragraph Break -/// Category: layout -#[element(Unlabellable)] +#[elem(title = "Paragraph Break", Unlabellable)] pub struct ParbreakElem {} impl Unlabellable for ParbreakElem {} @@ -266,8 +259,8 @@ struct Preparation<'a> { hyphenate: Option<bool>, /// The text language if it's the same for all children. lang: Option<Lang>, - /// The paragraph's resolved alignment. - align: Align, + /// The paragraph's resolved horizontal alignment. + align: FixedAlign, /// Whether to justify the paragraph. justify: bool, /// The paragraph's hanging indent. @@ -550,7 +543,7 @@ fn collect<'a>( let first_line_indent = ParElem::first_line_indent_in(*styles); if !first_line_indent.is_zero() && consecutive - && AlignElem::alignment_in(*styles).x.resolve(*styles) + && AlignElem::alignment_in(*styles).resolve(*styles).x == TextElem::dir_in(*styles).start().into() { full.push(SPACING_REPLACE); @@ -593,15 +586,15 @@ fn collect<'a>( let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; full.push(c); Segment::Text(c.len_utf8()) - } else if let Some(elem) = child.to::<SmartQuoteElem>() { + } else if let Some(elem) = child.to::<SmartquoteElem>() { let prev = full.len(); - if SmartQuoteElem::enabled_in(styles) { + if SmartquoteElem::enabled_in(styles) { let lang = TextElem::lang_in(styles); let region = TextElem::region_in(styles); let quotes = Quotes::from_lang( lang, region, - SmartQuoteElem::alternative_in(styles), + SmartquoteElem::alternative_in(styles), ); let peeked = iter.peek().and_then(|child| { let child = if let Some((child, _)) = child.to_styled() { @@ -611,7 +604,7 @@ fn collect<'a>( }; if let Some(elem) = child.to::<TextElem>() { elem.text().chars().next() - } else if child.is::<SmartQuoteElem>() { + } else if child.is::<SmartquoteElem>() { Some('"') } else if child.is::<SpaceElem>() || child.is::<HElem>() @@ -642,7 +635,7 @@ fn collect<'a>( }; if let Some(last) = full.chars().last() { - quoter.last(last, child.is::<SmartQuoteElem>()); + quoter.last(last, child.is::<SmartquoteElem>()); } spans.push(segment.len(), child.span()); @@ -673,9 +666,10 @@ fn prepare<'a>( styles: StyleChain<'a>, region: Size, ) -> SourceResult<Preparation<'a>> { + let dir = TextElem::dir_in(styles); let bidi = BidiInfo::new( text, - match TextElem::dir_in(styles) { + match dir { Dir::LTR => Some(BidiLevel::ltr()), Dir::RTL => Some(BidiLevel::rtl()), _ => None, @@ -734,7 +728,7 @@ fn prepare<'a>( styles, hyphenate: shared_get(styles, children, TextElem::hyphenate_in), lang: shared_get(styles, children, TextElem::lang_in), - align: AlignElem::alignment_in(styles).x.resolve(styles), + align: AlignElem::alignment_in(styles).resolve(styles).x, justify: ParElem::justify_in(styles), hang: ParElem::hanging_indent_in(styles), }) diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs index 95c042ff..39a38b16 100644 --- a/crates/typst-library/src/layout/place.rs +++ b/crates/typst-library/src/layout/place.rs @@ -7,7 +7,7 @@ use crate::prelude::*; /// other content in the container. Page margins will be respected. /// /// -/// ## Example { #example } +/// # Example /// ```example /// #set page(height: 60pt) /// Hello, world! @@ -20,10 +20,7 @@ use crate::prelude::*; /// ), /// ) /// ``` -/// -/// Display: Place -/// Category: layout -#[element(Layout, Behave)] +#[elem(Layout, Behave)] pub struct PlaceElem { /// Relative to which position in the parent container to place the content. /// @@ -34,8 +31,8 @@ pub struct PlaceElem { /// that axis will be ignored, instead, the item will be placed in the /// origin of the axis. #[positional] - #[default(Smart::Custom(Axes::with_x(Some(GenAlign::Start))))] - pub alignment: Smart<Axes<Option<GenAlign>>>, + #[default(Smart::Custom(Align::START))] + pub alignment: Smart<Align>, /// Whether the placed element has floating layout. /// @@ -98,16 +95,7 @@ impl Layout for PlaceElem { let float = self.float(styles); let alignment = self.alignment(styles); - if float - && !matches!( - alignment, - Smart::Auto - | Smart::Custom(Axes { - y: Some(GenAlign::Specific(Align::Top | Align::Bottom)), - .. - }) - ) - { + if float && alignment.map_or(false, |align| align.y() == Some(VAlign::Horizon)) { bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`"); } else if !float && alignment.is_auto() { return Err("automatic positioning is only available for floating placement") @@ -115,9 +103,7 @@ impl Layout for PlaceElem { .at(self.span()); } - let child = self.body().aligned( - alignment.unwrap_or_else(|| Axes::with_x(Some(Align::Center.into()))), - ); + let child = self.body().aligned(alignment.unwrap_or_else(|| Align::CENTER)); let pod = Regions::one(base, Axes::splat(false)); let frame = child.layout(vt, styles, pod)?.into_frame(); diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index 646eb991..41dede51 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -12,7 +12,7 @@ use super::AlignElem; /// Errors if there no bounds on the available space, as it would create /// infinite content. /// -/// ## Example { #example } +/// # Example /// ```example /// Sign on the dotted line: /// #box(width: 1fr, repeat[.]) @@ -23,10 +23,7 @@ use super::AlignElem; /// Berlin, the 22nd of December, 2022 /// ] /// ``` -/// -/// Display: Repeat -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct RepeatElem { /// The content to repeat. #[required] @@ -43,7 +40,7 @@ impl Layout for RepeatElem { ) -> SourceResult<Fragment> { let pod = Regions::one(regions.size, Axes::new(false, false)); let piece = self.body().layout(vt, styles, pod)?.into_frame(); - let align = AlignElem::alignment_in(styles).x.resolve(styles); + let align = AlignElem::alignment_in(styles).resolve(styles); let fill = regions.size.x; let width = piece.width(); @@ -64,7 +61,7 @@ impl Layout for RepeatElem { let mut offset = Abs::zero(); if count == 1.0 { - offset += align.position(remaining); + offset += align.x.position(remaining); } if width > Abs::zero() { diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs index 69a5d952..868b3d50 100644 --- a/crates/typst-library/src/layout/spacing.rs +++ b/crates/typst-library/src/layout/spacing.rs @@ -8,20 +8,17 @@ use crate::prelude::*; /// remaining space on the line is distributed among all fractional spacings /// according to their relative fractions. /// -/// ## Example { #example } +/// # Example /// ```example /// First #h(1cm) Second \ /// First #h(30%) Second \ /// First #h(2fr) Second #h(1fr) Third /// ``` /// -/// ## Mathematical Spacing { #math-spacing } +/// # Mathematical Spacing { #math-spacing } /// In [mathematical formulas]($category/math), you can additionally use these /// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`. -/// -/// Display: Spacing (H) -/// Category: layout -#[element(Behave)] +#[elem(title = "Spacing (H)", Behave)] pub struct HElem { /// How much spacing to insert. #[required] @@ -79,7 +76,7 @@ impl Behave for HElem { /// the remaining space on the page is distributed among all fractional spacings /// according to their relative fractions. /// -/// ## Example { #example } +/// # Example /// ```example /// #grid( /// rows: 3cm, @@ -93,10 +90,7 @@ impl Behave for HElem { /// [A #v(1fr) B], /// ) /// ``` -/// -/// Display: Spacing (V) -/// Category: layout -#[element(Behave)] +#[elem(title = "Spacing (V)", Behave)] pub struct VElem { /// How much spacing to insert. #[required] diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs index 52a2f289..d3fcba8d 100644 --- a/crates/typst-library/src/layout/stack.rs +++ b/crates/typst-library/src/layout/stack.rs @@ -6,7 +6,7 @@ use crate::prelude::*; /// The stack places a list of items along an axis, with optional spacing /// between each item. /// -/// ## Example { #example } +/// # Example /// ```example /// #stack( /// dir: ttb, @@ -15,10 +15,7 @@ use crate::prelude::*; /// rect(width: 90pt), /// ) /// ``` -/// -/// Display: Stack -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct StackElem { /// The direction along which the items are stacked. Possible values are: /// @@ -27,7 +24,7 @@ pub struct StackElem { /// - `{ttb}`: Top to bottom. /// - `{btt}`: Bottom to top. /// - /// You cab use the `start` and `end` methods to obtain the initial and + /// You can use the `start` and `end` methods to obtain the initial and /// final points (respectively) of a direction, as `alignment`. You can also /// use the `axis` method to determine whether a direction is /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a @@ -141,7 +138,7 @@ enum StackItem { /// Fractional spacing between other items. Fractional(Fr), /// A frame for a layouted block. - Frame(Frame, Axes<Align>), + Frame(Frame, Axes<FixedAlign>), } impl<'a> StackLayouter<'a> { @@ -204,7 +201,7 @@ impl<'a> StackLayouter<'a> { } // Block-axis alignment of the `AlignElement` is respected by stacks. - let aligns = if let Some(align) = block.to::<AlignElem>() { + let align = if let Some(align) = block.to::<AlignElem>() { align.alignment(styles) } else if let Some((_, local)) = block.to_styled() { AlignElem::alignment_in(styles.chain(local)) @@ -230,7 +227,7 @@ impl<'a> StackLayouter<'a> { self.used.main += gen.main; self.used.cross.set_max(gen.cross); - self.items.push(StackItem::Frame(frame, aligns)); + self.items.push(StackItem::Frame(frame, align)); if i + 1 < len { self.finish_region(); @@ -259,18 +256,18 @@ impl<'a> StackLayouter<'a> { let mut output = Frame::new(size); let mut cursor = Abs::zero(); - let mut ruler: Align = self.dir.start().into(); + let mut ruler: FixedAlign = self.dir.start().into(); // Place all frames. for item in self.items.drain(..) { match item { StackItem::Absolute(v) => cursor += v, StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), - StackItem::Frame(frame, aligns) => { + StackItem::Frame(frame, align) => { if self.dir.is_positive() { - ruler = ruler.max(aligns.get(self.axis)); + ruler = ruler.max(align.get(self.axis)); } else { - ruler = ruler.min(aligns.get(self.axis)); + ruler = ruler.min(align.get(self.axis)); } // Align along the main axis. @@ -285,7 +282,7 @@ impl<'a> StackLayouter<'a> { // Align along the cross axis. let other = self.axis.other(); - let cross = aligns + let cross = align .get(other) .position(size.get(other) - frame.size().get(other)); diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs index a7bc8a0e..4bbe79a6 100644 --- a/crates/typst-library/src/layout/table.rs +++ b/crates/typst-library/src/layout/table.rs @@ -9,13 +9,13 @@ use crate::prelude::*; /// Tables are used to arrange content in cells. Cells can contain arbitrary /// content, including multiple paragraphs and are specified in row-major order. /// Because tables are just grids with configurable cell properties, refer to -/// the [grid documentation]($func/grid) for more information on how to size the +/// the [grid documentation]($grid) for more information on how to size the /// table tracks. /// -/// To give a table a caption and make it [referenceable]($func/ref), put it -/// into a [figure]($func/figure). +/// To give a table a caption and make it [referenceable]($ref), put it into a +/// [figure]($figure). /// -/// ## Example { #example } +/// # Example /// ```example /// #table( /// columns: (1fr, auto, auto), @@ -34,34 +34,31 @@ use crate::prelude::*; /// [$a$: edge length] /// ) /// ``` -/// -/// Display: Table -/// Category: layout -#[element(Layout, LocalName, Figurable)] +#[elem(Layout, LocalName, Figurable)] pub struct TableElem { - /// The column sizes. See the [grid documentation]($func/grid) for more + /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. pub columns: TrackSizings, - /// The row sizes. See the [grid documentation]($func/grid) for more - /// information on track sizing. + /// The row sizes. See the [grid documentation]($grid) for more information + /// on track sizing. pub rows: TrackSizings, - /// The gaps between rows & columns. See the [grid - /// documentation]($func/grid) for more information on gutters. + /// The gaps between rows & columns. See the [grid documentation]($grid) for + /// more information on gutters. #[external] pub gutter: TrackSizings, - /// The gaps between columns. Takes precedence over `gutter`. See the [grid - /// documentation]($func/grid) for more information on gutters. + /// The gaps between columns. Takes precedence over `gutter`. See the + /// [grid documentation]($grid) for more information on gutters. #[parse( let gutter = args.named("gutter")?; args.named("column-gutter")?.or_else(|| gutter.clone()) )] pub column_gutter: TrackSizings, - /// The gaps between rows. Takes precedence over `gutter`. See the [grid - /// documentation]($func/grid) for more information on gutters. + /// The gaps between rows. Takes precedence over `gutter`. See the + /// [grid documentation]($grid) for more information on gutters. #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] pub row_gutter: TrackSizings, @@ -102,20 +99,19 @@ pub struct TableElem { /// [A], [B], [C], /// ) /// ``` - pub align: Celled<Smart<Axes<Option<GenAlign>>>>, + pub align: Celled<Smart<Align>>, - /// How to stroke the cells. + /// How to [stroke]($stroke) the cells. /// - /// See the [line's documentation]($func/line.stroke) for more details. /// Strokes can be disabled by setting this to `{none}`. /// /// _Note:_ Richer stroke customization for individual cells is not yet - /// implemented, but will be in the future. In the meantime, you can use - /// the third-party [tablex library](https://github.com/PgBiel/typst-tablex/). + /// implemented, but will be in the future. In the meantime, you can use the + /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). #[resolve] #[fold] - #[default(Some(PartialStroke::default()))] - pub stroke: Option<PartialStroke>, + #[default(Some(Stroke::default()))] + pub stroke: Option<Stroke>, /// How much to pad the cells' content. #[default(Abs::pt(5.0).into())] @@ -158,7 +154,7 @@ impl Layout for TableElem { .collect::<SourceResult<_>>()?; let fill = self.fill(styles); - let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default); + let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( @@ -268,8 +264,12 @@ impl<T: Default> Default for Celled<T> { } impl<T: Reflect> Reflect for Celled<T> { - fn describe() -> CastInfo { - T::describe() + Array::describe() + Func::describe() + fn input() -> CastInfo { + T::input() + Array::input() + Func::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + Func::output() } fn castable(value: &Value) -> bool { diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs index d693f100..07f17bb0 100644 --- a/crates/typst-library/src/layout/terms.rs +++ b/crates/typst-library/src/layout/terms.rs @@ -8,29 +8,22 @@ use crate::prelude::*; /// descriptions span over multiple lines, they use hanging indent to /// communicate the visual hierarchy. /// -/// ## Example { #example } +/// # Example /// ```example /// / Ligature: A merged glyph. /// / Kerning: A spacing adjustment /// between two adjacent letters. /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: Starting a line with a slash, /// followed by a term, a colon and a description creates a term list item. -/// -/// Display: Term List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", TermItem::func()); - scope -)] +#[elem(scope, title = "Term List", Layout)] pub struct TermsElem { - /// If this is `{false}`, the items are spaced apart with [term list - /// spacing]($func/terms.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the term list more - /// compact, which can look better if the items are short. + /// If this is `{false}`, the items are spaced apart with + /// [term list spacing]($terms.spacing). If it is `{true}`, they use normal + /// [leading]($par.leading) instead. This makes the term list more compact, + /// which can look better if the items are short. /// /// In markup mode, the value of this parameter is determined based on /// whether items are separated with a blank line. If items directly follow @@ -81,7 +74,7 @@ pub struct TermsElem { /// The spacing between the items of a wide (non-tight) term list. /// - /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + /// If set to `{auto}`, uses the spacing [below blocks]($block.below). pub spacing: Smart<Spacing>, /// The term list's children. @@ -100,6 +93,12 @@ pub struct TermsElem { pub children: Vec<TermItem>, } +#[scope] +impl TermsElem { + #[elem] + type TermItem; +} + impl Layout for TermsElem { #[tracing::instrument(name = "TermsElem::layout", skip_all)] fn layout( @@ -138,10 +137,7 @@ impl Layout for TermsElem { } /// A term list item. -/// -/// Display: Term List Item -/// Category: layout -#[element] +#[elem(name = "item", title = "Term List Item")] pub struct TermItem { /// The term described by the list item. #[required] diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index a57a5edc..012a146d 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -8,7 +8,7 @@ use crate::prelude::*; /// it at the original positions. Containers will still be sized as if the /// content was not moved. /// -/// ## Example { #example } +/// # Example /// ```example /// #rect(inset: 0pt, move( /// dx: 6pt, dy: 6pt, @@ -20,10 +20,7 @@ use crate::prelude::*; /// ) /// )) /// ``` -/// -/// Display: Move -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct MoveElem { /// The horizontal displacement of the content. pub dx: Rel<Length>, @@ -47,7 +44,7 @@ impl Layout for MoveElem { let pod = Regions::one(regions.base(), Axes::splat(false)); let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles); - let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); + let delta = delta.zip_map(regions.base(), Rel::relative_to); frame.translate(delta.to_point()); Ok(Fragment::frame(frame)) } @@ -58,7 +55,7 @@ impl Layout for MoveElem { /// Rotates an element by a given angle. The layout will act as if the element /// was not rotated. /// -/// ## Example { #example } +/// # Example /// ```example /// #stack( /// dir: ltr, @@ -67,10 +64,7 @@ impl Layout for MoveElem { /// .map(i => rotate(24deg * i)[X]), /// ) /// ``` -/// -/// Display: Rotate -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct RotateElem { /// The amount of rotation. /// @@ -96,10 +90,9 @@ pub struct RotateElem { /// #box(rotate(30deg, origin: top + left, square())) /// #box(rotate(30deg, origin: bottom + right, square())) /// ``` - #[resolve] #[fold] - #[default(Align::CENTER_HORIZON)] - pub origin: Axes<Option<GenAlign>>, + #[default(HAlign::Center + VAlign::Horizon)] + pub origin: Align, /// The content to rotate. #[required] @@ -116,8 +109,10 @@ impl Layout for RotateElem { ) -> SourceResult<Fragment> { let pod = Regions::one(regions.base(), Axes::splat(false)); let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = - self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); + let Axes { x, y } = self + .origin(styles) + .resolve(styles) + .zip_map(frame.size(), FixedAlign::position); let ts = Transform::translate(x, y) .pre_concat(Transform::rotate(self.angle(styles))) .pre_concat(Transform::translate(-x, -y)); @@ -130,15 +125,12 @@ impl Layout for RotateElem { /// /// Lets you mirror content by specifying a negative scale on a single axis. /// -/// ## Example { #example } +/// # Example /// ```example /// #set align(center) /// #scale(x: -100%)[This is mirrored.] /// ``` -/// -/// Display: Scale -/// Category: layout -#[element(Layout)] +#[elem(Layout)] pub struct ScaleElem { /// The horizontal scaling factor. /// @@ -163,10 +155,9 @@ pub struct ScaleElem { /// A#box(scale(75%)[A])A \ /// B#box(scale(75%, origin: bottom + left)[B])B /// ``` - #[resolve] #[fold] - #[default(Align::CENTER_HORIZON)] - pub origin: Axes<Option<GenAlign>>, + #[default(HAlign::Center + VAlign::Horizon)] + pub origin: Align, /// The content to scale. #[required] @@ -183,8 +174,10 @@ impl Layout for ScaleElem { ) -> SourceResult<Fragment> { let pod = Regions::one(regions.base(), Axes::splat(false)); let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = - self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); + let Axes { x, y } = self + .origin(styles) + .resolve(styles) + .zip_map(frame.size(), FixedAlign::position); let transform = Transform::translate(x, y) .pre_concat(Transform::scale(self.x(styles), self.y(styles))) .pre_concat(Transform::translate(-x, -y)); diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index e9bb72ce..bdb97f84 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -14,10 +14,9 @@ pub mod symbols; pub mod text; pub mod visualize; -use typst::diag::At; -use typst::eval::{LangItems, Library, Module, Scope}; -use typst::geom::Smart; -use typst::model::{Element, Styles}; +use typst::eval::{Array, LangItems, Library, Module, Scope}; +use typst::geom::{Align, Color, Dir, Smart}; +use typst::model::{NativeElement, Styles}; use self::layout::LayoutRoot; @@ -32,17 +31,53 @@ pub fn build() -> Library { #[tracing::instrument(skip_all)] fn global(math: Module) -> Module { let mut global = Scope::deduplicating(); - - // Categories. text::define(&mut global); + global.define_module(math); layout::define(&mut global); visualize::define(&mut global); meta::define(&mut global); - compute::define(&mut global); symbols::define(&mut global); - global.define("math", math); + compute::define(&mut global); + prelude(&mut global); + Module::new("global", global) +} - Module::new("global").with_scope(global) +/// Defines scoped values that are globally available, too. +fn prelude(global: &mut Scope) { + global.define("black", Color::BLACK); + global.define("gray", Color::GRAY); + global.define("silver", Color::SILVER); + global.define("white", Color::WHITE); + global.define("navy", Color::NAVY); + global.define("blue", Color::BLUE); + global.define("aqua", Color::AQUA); + global.define("teal", Color::TEAL); + global.define("eastern", Color::EASTERN); + global.define("purple", Color::PURPLE); + global.define("fuchsia", Color::FUCHSIA); + global.define("maroon", Color::MAROON); + global.define("red", Color::RED); + global.define("orange", Color::ORANGE); + global.define("yellow", Color::YELLOW); + global.define("olive", Color::OLIVE); + global.define("green", Color::GREEN); + global.define("lime", Color::LIME); + global.define("luma", Color::luma_data()); + global.define("rgb", Color::rgb_data()); + global.define("cmyk", Color::cmyk_data()); + global.define("range", Array::range_data()); + global.define("ltr", Dir::LTR); + global.define("rtl", Dir::RTL); + global.define("ttb", Dir::TTB); + global.define("btt", Dir::BTT); + global.define("start", Align::START); + global.define("left", Align::LEFT); + global.define("center", Align::CENTER); + global.define("right", Align::RIGHT); + global.define("end", Align::END); + global.define("top", Align::TOP); + global.define("horizon", Align::HORIZON); + global.define("bottom", Align::BOTTOM); } /// Construct the standard style map. @@ -59,9 +94,9 @@ fn items() -> LangItems { space: || text::SpaceElem::new().pack(), linebreak: || text::LinebreakElem::new().pack(), text: |text| text::TextElem::new(text).pack(), - text_func: text::TextElem::func(), + text_elem: text::TextElem::elem(), text_str: |content| Some(content.to::<text::TextElem>()?.text()), - smart_quote: |double| text::SmartQuoteElem::new().with_double(double).pack(), + smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(), parbreak: || layout::ParbreakElem::new().pack(), strong: |body| text::StrongElem::new(body).pack(), emph: |body| text::EmphElem::new(body).pack(), @@ -85,7 +120,7 @@ fn items() -> LangItems { }, bibliography_keys: meta::BibliographyElem::keys, heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), - heading_func: meta::HeadingElem::func(), + heading_elem: meta::HeadingElem::elem(), list_item: |body| layout::ListItem::new(body).pack(), enum_item: |number, body| { let mut elem = layout::EnumItem::new(body); @@ -95,9 +130,6 @@ fn items() -> LangItems { elem.pack() }, term_item: |term, description| layout::TermItem::new(term, description).pack(), - rgb_func: compute::rgb_func(), - cmyk_func: compute::cmyk_func(), - luma_func: compute::luma_func(), equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), math_align_point: || math::AlignPointElem::new().pack(), math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), @@ -131,15 +163,5 @@ fn items() -> LangItems { math_root: |index, radicand| { math::RootElem::new(radicand).with_index(index).pack() }, - library_method: |vm, dynamic, method, args, span| { - if let Some(counter) = dynamic.downcast::<meta::Counter>().cloned() { - counter.call_method(vm, method, args, span) - } else if let Some(state) = dynamic.downcast::<meta::State>().cloned() { - state.call_method(vm, method, args, span) - } else { - Err(format!("type {} has no method `{method}`", dynamic.type_name())) - .at(span) - } - }, } } diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index d1bee198..c92f9585 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -5,16 +5,13 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// Attaches an accent to a base. /// -/// ## Example { #example } +/// # Example /// ```example /// $grave(a) = accent(a, `)$ \ /// $arrow(a) = accent(a, arrow)$ \ /// $tilde(a) = accent(a, \u{0303})$ /// ``` -/// -/// Display: Accent -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct AccentElem { /// The base to which the accent is applied. /// May consist of multiple letters. diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs index aee89a89..bf81597c 100644 --- a/crates/typst-library/src/math/align.rs +++ b/crates/typst-library/src/math/align.rs @@ -1,10 +1,7 @@ use super::*; /// A math alignment point: `&`, `&&`. -/// -/// Display: Alignment Point -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Alignment Point", LayoutMath)] pub struct AlignPointElem {} impl LayoutMath for AlignPointElem { diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs index d74beafe..c33b58e4 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst-library/src/math/attach.rs @@ -2,26 +2,13 @@ use super::*; /// A base with optional attachments. /// -/// ## Example { #example } /// ```example -/// // With syntax. -/// $ sum_(i=0)^n a_i = 2^(1+i) $ -/// -/// // With function call. /// $ attach( /// Pi, t: alpha, b: beta, /// tl: 1, tr: 2+3, bl: 4+5, br: 6, /// ) $ /// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax for attachments after the base: Use -/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the -/// hat (`^`) to indicate a superscript i.e. top attachment. -/// -/// Display: Attachment -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct AttachElem { /// The base to which things are attached. #[required] @@ -86,19 +73,15 @@ impl LayoutMath for AttachElem { /// Grouped primes. /// -/// ## Example { #example } /// ```example /// $ a'''_b = a^'''_b $ /// ``` /// -/// ## Syntax +/// # Syntax /// This function has dedicated syntax: use apostrophes instead of primes. They /// will automatically attach to the previous element, moving superscripts to /// the next level. -/// -/// Display: Attachment -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct PrimesElem { /// The number of grouped primes. #[required] @@ -141,14 +124,10 @@ impl LayoutMath for PrimesElem { /// Forces a base to display attachments as scripts. /// -/// ## Example { #example } /// ```example /// $ scripts(sum)_1^2 != sum_1^2 $ /// ``` -/// -/// Display: Scripts -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct ScriptsElem { /// The base to attach the scripts to. #[required] @@ -167,14 +146,10 @@ impl LayoutMath for ScriptsElem { /// Forces a base to display attachments as limits. /// -/// ## Example { #example } /// ```example /// $ limits(A)_1^2 != A_1^2 $ /// ``` -/// -/// Display: Limits -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct LimitsElem { /// The base to attach the limits to. #[required] diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs index f576a727..d27031b9 100644 --- a/crates/typst-library/src/math/cancel.rs +++ b/crates/typst-library/src/math/cancel.rs @@ -4,17 +4,14 @@ use super::*; /// /// This is commonly used to show the elimination of a term. /// -/// ## Example { #example } +/// # Example /// ```example /// >>> #set page(width: 140pt) /// Here, we can simplify: /// $ (a dot b dot cancel(x)) / /// cancel(x) $ /// ``` -/// -/// Display: Cancel -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct CancelElem { /// The content over which the line should be placed. #[required] @@ -53,8 +50,8 @@ pub struct CancelElem { #[default(false)] pub cross: bool, - /// How to rotate the cancel line. See the [line's - /// documentation]($func/line.angle) for more details. + /// How to rotate the cancel line. See the + /// [line's documentation]($line.angle) for more details. /// /// ```example /// >>> #set page(width: 140pt) @@ -63,8 +60,7 @@ pub struct CancelElem { #[default(Angle::zero())] pub rotation: Angle, - /// How to stroke the cancel line. See the - /// [line's documentation]($func/line.stroke) for more details. + /// How to [stroke]($stroke) the cancel line. /// /// ```example /// >>> #set page(width: 140pt) @@ -79,12 +75,12 @@ pub struct CancelElem { /// ``` #[resolve] #[fold] - #[default(PartialStroke { + #[default(Stroke { // Default stroke has 0.5pt for better visuals. thickness: Smart::Custom(Abs::pt(0.5)), ..Default::default() })] - pub stroke: PartialStroke, + pub stroke: Stroke, } impl LayoutMath for CancelElem { @@ -99,7 +95,7 @@ impl LayoutMath for CancelElem { let span = self.span(); let length = self.length(styles).resolve(styles); - let stroke = self.stroke(styles).unwrap_or(Stroke { + let stroke = self.stroke(styles).unwrap_or(FixedStroke { paint: TextElem::fill_in(styles), ..Default::default() }); @@ -139,7 +135,7 @@ impl LayoutMath for CancelElem { /// Draws a cancel line. fn draw_cancel_line( length: Rel<Abs>, - stroke: Stroke, + stroke: FixedStroke, invert: bool, angle: Angle, body_size: Size, @@ -172,8 +168,8 @@ fn draw_cancel_line( // (-width / 2, height / 2) with length components (width, -height) (sign is // inverted in the y-axis). After applying the scale, the line will have the // correct length and orientation (inverted if needed). - let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s); - let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s); + let start = Axes::new(-mid.x, mid.y).zip_map(scales, |l, s| l * s); + let delta = Axes::new(width, -height).zip_map(scales, |l, s| l * s); let mut frame = Frame::new(body_size); frame.push( diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs index 69635c62..fc8a6c79 100644 --- a/crates/typst-library/src/math/class.rs +++ b/crates/typst-library/src/math/class.rs @@ -5,7 +5,7 @@ use super::*; /// This is useful to treat certain symbols as if they were of a different /// class, e.g. to make a symbol behave like a relation. /// -/// ## Example { #example } +/// # Example /// ```example /// #let loves = math.class( /// "relation", @@ -14,10 +14,7 @@ use super::*; /// /// $x loves y and y loves 5$ /// ``` -/// -/// Display: Class -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct ClassElem { /// The class to apply to the content. #[required] diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs index cf1d38e9..6a296203 100644 --- a/crates/typst-library/src/math/frac.rs +++ b/crates/typst-library/src/math/frac.rs @@ -4,21 +4,18 @@ const FRAC_AROUND: Em = Em::new(0.1); /// A mathematical fraction. /// -/// ## Example { #example } +/// # Example /// ```example /// $ 1/2 < (x+1)/2 $ /// $ ((x+1)) / 2 = frac(a, b) $ /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: Use a slash to turn neighbouring /// expressions into a fraction. Multiple atoms can be grouped into a single /// expression using round grouping parenthesis. Such parentheses are removed /// from the output, but you can nest multiple to force them. -/// -/// Display: Fraction -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Fraction", LayoutMath)] pub struct FracElem { /// The fraction's numerator. #[required] @@ -38,14 +35,11 @@ impl LayoutMath for FracElem { /// A binomial expression. /// -/// ## Example { #example } +/// # Example /// ```example /// $ binom(n, k) $ /// ``` -/// -/// Display: Binomial -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Binomial", LayoutMath)] pub struct BinomElem { /// The binomial's upper index. #[required] @@ -135,10 +129,10 @@ fn layout( frame.push( line_pos, FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked(Stroke { + Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { paint: TextElem::fill_in(ctx.styles()), thickness, - ..Stroke::default() + ..FixedStroke::default() }), span, ), diff --git a/crates/typst-library/src/math/delimited.rs b/crates/typst-library/src/math/lr.rs index 25ecf623..0d3c855e 100644 --- a/crates/typst-library/src/math/delimited.rs +++ b/crates/typst-library/src/math/lr.rs @@ -7,16 +7,7 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); /// /// While matched delimiters scale by default, this can be used to scale /// unmatched delimiters and to control the delimiter scaling more precisely. -/// -/// ## Example { #example } -/// ```example -/// $ lr(]a, b/2]) $ -/// $ lr(]sum_(x=1)^n] x, size: #50%) $ -/// ``` -/// -/// Display: Left/Right -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Left/Right", LayoutMath)] pub struct LrElem { /// The size of the brackets, relative to the height of the wrapped content. pub size: Smart<Rel<Length>>, @@ -107,13 +98,9 @@ fn scale( /// Floors an expression. /// -/// ## Example { #example } /// ```example /// $ floor(x/2) $ /// ``` -/// -/// Display: Floor -/// Category: math #[func] pub fn floor( /// The expression to floor. @@ -124,13 +111,9 @@ pub fn floor( /// Ceils an expression. /// -/// ## Example { #example } /// ```example /// $ ceil(x/2) $ /// ``` -/// -/// Display: Ceil -/// Category: math #[func] pub fn ceil( /// The expression to ceil. @@ -141,13 +124,9 @@ pub fn ceil( /// Rounds an expression. /// -/// ## Example { #example } /// ```example /// $ round(x/2) $ /// ``` -/// -/// Display: Round -/// Category: math #[func] pub fn round( /// The expression to round. @@ -158,14 +137,9 @@ pub fn round( /// Takes the absolute value of an expression. /// -/// ## Example { #example } /// ```example /// $ abs(x/2) $ /// ``` -/// -/// -/// Display: Abs -/// Category: math #[func] pub fn abs( /// The expression to take the absolute value of. @@ -176,13 +150,9 @@ pub fn abs( /// Takes the norm of an expression. /// -/// ## Example { #example } /// ```example /// $ norm(x/2) $ /// ``` -/// -/// Display: Norm -/// Category: math #[func] pub fn norm( /// The expression to take the norm of. diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index c913592d..abb0da35 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -12,15 +12,12 @@ const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); /// /// Content in the vector's elements can be aligned with the `&` symbol. /// -/// ## Example { #example } +/// # Example /// ```example /// $ vec(a, b, c) dot vec(1, 2, 3) /// = a + 2b + 3c $ /// ``` -/// -/// Display: Vector -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Vector", LayoutMath)] pub struct VecElem { /// The delimiter to use. /// @@ -40,7 +37,7 @@ impl LayoutMath for VecElem { #[tracing::instrument(skip(ctx))] fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { let delim = self.delim(ctx.styles()); - let frame = layout_vec_body(ctx, &self.children(), Align::Center)?; + let frame = layout_vec_body(ctx, &self.children(), FixedAlign::Center)?; layout_delimiters( ctx, frame, @@ -61,7 +58,7 @@ impl LayoutMath for VecElem { /// /// Content in cells that are in the same row can be aligned with the `&` symbol. /// -/// ## Example { #example } +/// # Example /// ```example /// $ mat( /// 1, 2, ..., 10; @@ -70,10 +67,7 @@ impl LayoutMath for VecElem { /// 10, 10, ..., 10; /// ) $ /// ``` -/// -/// Display: Matrix -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Matrix", LayoutMath)] pub struct MatElem { /// The delimiter to use. /// @@ -102,10 +96,8 @@ pub struct MatElem { /// drawn after the second column of the matrix. Accepts either an /// integer for a single line, or an array of integers /// for multiple lines. - /// - `stroke`: How to stroke the line. See the - /// [line's documentation]($func/line.stroke) - /// for more details. If set to `{auto}`, takes on a thickness of - /// 0.05em and square line caps. + /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`, + /// takes on a thickness of 0.05em and square line caps. /// /// ```example /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ @@ -204,7 +196,7 @@ impl LayoutMath for MatElem { /// /// Content across different branches can be aligned with the `&` symbol. /// -/// ## Example { #example } +/// # Example /// ```example /// $ f(x, y) := cases( /// 1 "if" (x dot y)/2 <= 0, @@ -213,10 +205,7 @@ impl LayoutMath for MatElem { /// 4 "else", /// ) $ /// ``` -/// -/// Display: Cases -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct CasesElem { /// The delimiter to use. /// @@ -236,7 +225,7 @@ impl LayoutMath for CasesElem { #[tracing::instrument(skip(ctx))] fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { let delim = self.delim(ctx.styles()); - let frame = layout_vec_body(ctx, &self.children(), Align::Left)?; + let frame = layout_vec_body(ctx, &self.children(), FixedAlign::Start)?; layout_delimiters(ctx, frame, Some(delim.open()), None, self.span()) } } @@ -289,7 +278,7 @@ impl Delimiter { fn layout_vec_body( ctx: &mut MathContext, column: &[Content], - align: Align, + align: FixedAlign, ) -> SourceResult<Frame> { let gap = ROW_GAP.scaled(ctx); ctx.style(ctx.style.for_denominator()); @@ -319,7 +308,7 @@ fn layout_mat_body( // look correct by default at all matrix sizes. // The line cap is also set to square because it looks more "correct". let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx); - let default_stroke = Stroke { + let default_stroke = FixedStroke { thickness: default_stroke_thickness, line_cap: LineCap::Square, ..Default::default() @@ -383,7 +372,7 @@ fn layout_mat_body( let mut y = Abs::zero(); for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_aligned_frame(ctx, &points, Align::Center); + let cell = cell.into_aligned_frame(ctx, &points, FixedAlign::Center); let pos = Point::new( if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, y + ascent - cell.ascent(), @@ -429,7 +418,7 @@ fn layout_mat_body( Ok(frame) } -fn line_item(length: Abs, vertical: bool, stroke: Stroke, span: Span) -> FrameItem { +fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem { let line_geom = if vertical { Geometry::Line(Point::with_y(length)) } else { @@ -482,14 +471,14 @@ fn layout_delimiters( /// Parameters specifying how augmentation lines /// should be drawn on a matrix. #[derive(Default, Clone, Hash)] -pub struct Augment<T = Length> { +pub struct Augment<T: Numeric = Length> { pub hline: Offsets, pub vline: Offsets, - pub stroke: Smart<PartialStroke<T>>, + pub stroke: Smart<Stroke<T>>, } impl Augment<Abs> { - fn stroke_or(&self, fallback: Stroke) -> Stroke { + fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke { match &self.stroke { Smart::Custom(v) => v.clone().unwrap_or(fallback), _ => fallback, @@ -543,7 +532,7 @@ cast! { let vline = dict.take("vline").ok().map(Offsets::from_value) .transpose().unwrap_or_default().unwrap_or_default(); - let stroke = dict.take("stroke").ok().map(PartialStroke::from_value) + let stroke = dict.take("stroke").ok().map(Stroke::from_value) .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto); Augment { hline, vline, stroke } diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5d32af64..578064ba 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -7,9 +7,9 @@ mod align; mod attach; mod cancel; mod class; -mod delimited; mod frac; mod fragment; +mod lr; mod matrix; mod op; mod root; @@ -24,8 +24,8 @@ pub use self::align::*; pub use self::attach::*; pub use self::cancel::*; pub use self::class::*; -pub use self::delimited::*; pub use self::frac::*; +pub use self::lr::*; pub use self::matrix::*; pub use self::op::*; pub use self::root::*; @@ -57,79 +57,64 @@ use crate::text::{ /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.define("equation", EquationElem::func()); - math.define("text", TextElem::func()); - - // Grouping. - math.define("lr", LrElem::func()); - math.define("abs", abs_func()); - math.define("norm", norm_func()); - math.define("floor", floor_func()); - math.define("ceil", ceil_func()); - math.define("round", round_func()); - - // Attachments and accents. - math.define("attach", AttachElem::func()); - math.define("scripts", ScriptsElem::func()); - math.define("limits", LimitsElem::func()); - math.define("accent", AccentElem::func()); - math.define("underline", UnderlineElem::func()); - math.define("overline", OverlineElem::func()); - math.define("underbrace", UnderbraceElem::func()); - math.define("overbrace", OverbraceElem::func()); - math.define("underbracket", UnderbracketElem::func()); - math.define("overbracket", OverbracketElem::func()); - math.define("cancel", CancelElem::func()); - - // Fractions and matrix-likes. - math.define("frac", FracElem::func()); - math.define("binom", BinomElem::func()); - math.define("vec", VecElem::func()); - math.define("mat", MatElem::func()); - math.define("cases", CasesElem::func()); - - // Roots. - math.define("sqrt", sqrt_func()); - math.define("root", RootElem::func()); - - // Styles. - math.define("upright", upright_func()); - math.define("bold", bold_func()); - math.define("italic", italic_func()); - math.define("serif", serif_func()); - math.define("sans", sans_func()); - math.define("cal", cal_func()); - math.define("frak", frak_func()); - math.define("mono", mono_func()); - math.define("bb", bb_func()); - - math.define("display", display_func()); - math.define("inline", inline_func()); - math.define("script", script_func()); - math.define("sscript", sscript_func()); - - math.define("class", ClassElem::func()); - - // Text operators. - math.define("op", OpElem::func()); + math.category("math"); + math.define_elem::<EquationElem>(); + math.define_elem::<TextElem>(); + math.define_elem::<LrElem>(); + math.define_elem::<AttachElem>(); + math.define_elem::<ScriptsElem>(); + math.define_elem::<LimitsElem>(); + math.define_elem::<AccentElem>(); + math.define_elem::<UnderlineElem>(); + math.define_elem::<OverlineElem>(); + math.define_elem::<UnderbraceElem>(); + math.define_elem::<OverbraceElem>(); + math.define_elem::<UnderbracketElem>(); + math.define_elem::<OverbracketElem>(); + math.define_elem::<CancelElem>(); + math.define_elem::<FracElem>(); + math.define_elem::<BinomElem>(); + math.define_elem::<VecElem>(); + math.define_elem::<MatElem>(); + math.define_elem::<CasesElem>(); + math.define_elem::<RootElem>(); + math.define_elem::<ClassElem>(); + math.define_elem::<OpElem>(); + math.define_func::<abs>(); + math.define_func::<norm>(); + math.define_func::<floor>(); + math.define_func::<ceil>(); + math.define_func::<round>(); + math.define_func::<sqrt>(); + math.define_func::<upright>(); + math.define_func::<bold>(); + math.define_func::<italic>(); + math.define_func::<serif>(); + math.define_func::<sans>(); + math.define_func::<cal>(); + math.define_func::<frak>(); + math.define_func::<mono>(); + math.define_func::<bb>(); + math.define_func::<display>(); + math.define_func::<inline>(); + math.define_func::<script>(); + math.define_func::<sscript>(); + + // Text operators, spacings, and symbols. op::define(&mut math); - - // Spacings. spacing::define(&mut math); - - // Symbols. for (name, symbol) in crate::symbols::SYM { math.define(*name, symbol.clone()); } - Module::new("math").with_scope(math) + Module::new("math", math) } /// A mathematical equation. /// /// Can be displayed inline with text or as a separate block. /// -/// ## Example { #example } +/// # Example /// ```example /// #set text(font: "New Computer Modern") /// @@ -142,16 +127,13 @@ pub fn module() -> Module { /// $ sum_(k=1)^n k = (n(n+1)) / 2 $ /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: Write mathematical markup within /// dollar signs to create an equation. Starting and ending the equation with at /// least one space lifts it into a separate block that is centered /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). -/// -/// Display: Equation -/// Category: math -#[element( +#[elem( Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable, Outlinable )] @@ -160,7 +142,7 @@ pub struct EquationElem { #[default(false)] pub block: bool, - /// How to [number]($func/numbering) block-level equations. + /// How to [number]($numbering) block-level equations. /// /// ```example /// #set math.equation(numbering: "(1)") @@ -216,9 +198,9 @@ impl Synthesize for EquationElem { impl Show for EquationElem { #[tracing::instrument(name = "EquationElem::show", skip_all)] fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.clone().pack().guarded(Guard::Base(Self::func())); + let mut realized = self.clone().pack().guarded(Guard::Base(Self::elem())); if self.block(styles) { - realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) + realized = realized.aligned(Align::CENTER); } Ok(realized) } @@ -264,7 +246,7 @@ impl Layout for EquationElem { if block { if let Some(numbering) = self.numbering(styles) { let pod = Regions::one(regions.base(), Axes::splat(false)); - let counter = Counter::of(Self::func()) + let counter = Counter::of(Self::elem()) .display(Some(numbering), false) .layout(vt, styles, pod)? .into_frame(); @@ -277,7 +259,7 @@ impl Layout for EquationElem { }; let height = frame.height().max(counter.height()); - frame.resize(Size::new(width, height), Align::CENTER_HORIZON); + frame.resize(Size::new(width, height), Axes::splat(FixedAlign::Center)); let x = if TextElem::dir_in(styles).is_positive() { frame.width() - counter.width() @@ -358,7 +340,7 @@ impl Refable for EquationElem { } fn counter(&self) -> Counter { - Counter::of(Self::func()) + Counter::of(Self::elem()) } fn numbering(&self) -> Option<Numbering> { diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index 4016d24f..eed16465 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -4,23 +4,20 @@ use super::*; /// A text operator in an equation. /// -/// ## Example { #example } +/// # Example /// ```example /// $ tan x = (sin x)/(cos x) $ /// $ op("custom", /// limits: #true)_(n->oo) n $ /// ``` /// -/// ## Predefined Operators { #predefined } +/// # Predefined Operators { #predefined } /// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, /// `cosh`, `cot`, `coth`, `csc`, `ctg`, `deg`, `det`, `dim`, `exp`, `gcd`, /// `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, `ln`, /// `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`, /// `tan`, `tanh`, `tg` and `tr`. -/// -/// Display: Text Operator -/// Category: math -#[element(LayoutMath)] +#[elem(title = "Text Operator", LayoutMath)] pub struct OpElem { /// The operator's text. #[required] diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs index b889f1cb..03d0a212 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst-library/src/math/root.rs @@ -2,14 +2,10 @@ use super::*; /// A square root. /// -/// ## Example { #example } /// ```example /// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ /// ``` -/// -/// Display: Square Root -/// Category: math -#[func] +#[func(title = "Square Root")] pub fn sqrt( /// The expression to take the square root of. radicand: Content, @@ -19,14 +15,10 @@ pub fn sqrt( /// A general root. /// -/// ## Example { #example } /// ```example /// $ root(3, x) $ /// ``` -/// -/// Display: Root -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct RootElem { /// Which root of the radicand to take. #[positional] @@ -129,10 +121,10 @@ fn layout( frame.push( line_pos, FrameItem::Shape( - Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke { + Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke { paint: TextElem::fill_in(ctx.styles()), thickness, - ..Stroke::default() + ..FixedStroke::default() }), span, ), diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs index 687f82b8..cf3a8af2 100644 --- a/crates/typst-library/src/math/row.rs +++ b/crates/typst-library/src/math/row.rs @@ -121,7 +121,7 @@ impl MathRow { pub fn into_frame(self, ctx: &MathContext) -> Frame { let styles = ctx.styles(); - let align = AlignElem::alignment_in(styles).x.resolve(styles); + let align = AlignElem::alignment_in(styles).resolve(styles).x; self.into_aligned_frame(ctx, &[], align) } @@ -137,53 +137,54 @@ impl MathRow { self, ctx: &MathContext, points: &[Abs], - align: Align, + align: FixedAlign, ) -> Frame { - if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { - let leading = if ctx.style.size >= MathSize::Text { - ParElem::leading_in(ctx.styles()) - } else { - TIGHT_LEADING.scaled(ctx) - }; + if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + return self.into_line_frame(points, align); + } - let mut rows: Vec<_> = self.rows(); + let leading = if ctx.style.size >= MathSize::Text { + ParElem::leading_in(ctx.styles()) + } else { + TIGHT_LEADING.scaled(ctx) + }; - if matches!(rows.last(), Some(row) if row.0.is_empty()) { - rows.pop(); - } + let mut rows: Vec<_> = self.rows(); - let AlignmentResult { points, width } = alignments(&rows); - let mut frame = Frame::new(Size::zero()); + if matches!(rows.last(), Some(row) if row.0.is_empty()) { + rows.pop(); + } - for (i, row) in rows.into_iter().enumerate() { - let sub = row.into_line_frame(&points, align); - let size = frame.size_mut(); - if i > 0 { - size.y += leading; - } + let AlignmentResult { points, width } = alignments(&rows); + let mut frame = Frame::new(Size::zero()); - let mut pos = Point::with_y(size.y); - if points.is_empty() { - pos.x = align.position(width - sub.width()); - } - size.y += sub.height(); - size.x.set_max(sub.width()); - frame.push_frame(pos, sub); + for (i, row) in rows.into_iter().enumerate() { + let sub = row.into_line_frame(&points, align); + let size = frame.size_mut(); + if i > 0 { + size.y += leading; } - frame - } else { - self.into_line_frame(points, align) + + let mut pos = Point::with_y(size.y); + if points.is_empty() { + pos.x = align.position(width - sub.width()); + } + size.y += sub.height(); + size.x.set_max(sub.width()); + frame.push_frame(pos, sub); } + + frame } - fn into_line_frame(self, points: &[Abs], align: Align) -> Frame { + fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame { let ascent = self.ascent(); let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent())); frame.set_baseline(ascent); let mut next_x = { let mut widths = Vec::new(); - if !points.is_empty() && align != Align::Left { + if !points.is_empty() && align != FixedAlign::Start { let mut width = Abs::zero(); for fragment in self.iter() { if matches!(fragment, MathFragment::Align) { @@ -201,8 +202,10 @@ impl MathRow { let mut point_widths = points.iter().copied().zip(widths); let mut alternator = LeftRightAlternator::Right; move || match align { - Align::Left => prev_points.next(), - Align::Right => point_widths.next().map(|(point, width)| point - width), + FixedAlign::Start => prev_points.next(), + FixedAlign::End => { + point_widths.next().map(|(point, width)| point - width) + } _ => point_widths .next() .zip(prev_points.next()) diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs index 35a4cfdb..4d80a235 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst-library/src/math/style.rs @@ -2,13 +2,9 @@ use super::*; /// Bold font style in math. /// -/// ## Example { #example } /// ```example /// $ bold(A) := B^+ $ /// ``` -/// -/// Display: Bold -/// Category: math #[func] pub fn bold( /// The content to style. @@ -19,13 +15,9 @@ pub fn bold( /// Upright (non-italic) font style in math. /// -/// ## Example { #example } /// ```example /// $ upright(A) != A $ /// ``` -/// -/// Display: Upright -/// Category: math #[func] pub fn upright( /// The content to style. @@ -37,9 +29,6 @@ pub fn upright( /// Italic font style in math. /// /// For roman letters and greek lowercase letters, this is already the default. -/// -/// Display: Italic -/// Category: math #[func] pub fn italic( /// The content to style. @@ -50,9 +39,6 @@ pub fn italic( /// Serif (roman) font style in math. /// /// This is already the default. -/// -/// Display: Serif -/// Category: math #[func] pub fn serif( /// The content to style. @@ -63,14 +49,10 @@ pub fn serif( /// Sans-serif font style in math. /// -/// ## Example { #example } /// ```example /// $ sans(A B C) $ /// ``` -/// -/// Display: Sans-serif -/// Category: math -#[func] +#[func(title = "Sans Serif")] pub fn sans( /// The content to style. body: Content, @@ -80,14 +62,10 @@ pub fn sans( /// Calligraphic font style in math. /// -/// ## Example { #example } /// ```example /// Let $cal(P)$ be the set of ... /// ``` -/// -/// Display: Calligraphic -/// Category: math -#[func] +#[func(title = "Calligraphic")] pub fn cal( /// The content to style. body: Content, @@ -97,14 +75,10 @@ pub fn cal( /// Fraktur font style in math. /// -/// ## Example { #example } /// ```example /// $ frak(P) $ /// ``` -/// -/// Display: Fraktur -/// Category: math -#[func] +#[func(title = "Fraktur")] pub fn frak( /// The content to style. body: Content, @@ -114,14 +88,10 @@ pub fn frak( /// Monospace font style in math. /// -/// ## Example { #example } /// ```example /// $ mono(x + y = z) $ /// ``` -/// -/// Display: Monospace -/// Category: math -#[func] +#[func(title = "Monospace")] pub fn mono( /// The content to style. body: Content, @@ -134,16 +104,12 @@ pub fn mono( /// For uppercase latin letters, blackboard bold is additionally available /// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. /// -/// ## Example { #example } /// ```example /// $ bb(b) $ /// $ bb(N) = NN $ /// $ f: NN -> RR $ /// ``` -/// -/// Display: Blackboard Bold -/// Category: math -#[func] +#[func(title = "Blackboard Bold")] pub fn bb( /// The content to style. body: Content, @@ -155,14 +121,10 @@ pub fn bb( /// /// This is the normal size for block equations. /// -/// ## Example { #example } /// ```example /// $sum_i x_i/2 = display(sum_i x_i/2)$ /// ``` -/// -/// Display: Display Size -/// Category: math -#[func] +#[func(title = "Display Size")] pub fn display( /// The content to size. body: Content, @@ -182,15 +144,11 @@ pub fn display( /// /// This is the normal size for inline equations. /// -/// ## Example { #example } /// ```example /// $ sum_i x_i/2 /// = inline(sum_i x_i/2) $ /// ``` -/// -/// Display: Inline Size -/// Category: math -#[func] +#[func(title = "Inline Size")] pub fn inline( /// The content to size. body: Content, @@ -210,14 +168,10 @@ pub fn inline( /// /// This is the smaller size used in powers or sub- or superscripts. /// -/// ## Example { #example } /// ```example /// $sum_i x_i/2 = script(sum_i x_i/2)$ /// ``` -/// -/// Display: Script Size -/// Category: math -#[func] +#[func(title = "Script Size")] pub fn script( /// The content to size. body: Content, @@ -238,14 +192,10 @@ pub fn script( /// This is the smallest size, used in second-level sub- and superscripts /// (script of the script). /// -/// ## Example { #example } /// ```example /// $sum_i x_i/2 = sscript(sum_i x_i/2)$ /// ``` -/// -/// Display: Script-Script Size -/// Category: math -#[func] +#[func(title = "Script-Script Size")] pub fn sscript( /// The content to size. body: Content, @@ -262,10 +212,7 @@ pub fn sscript( } /// A font variant in math. -/// -/// Display: Bold -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct MathStyleElem { /// The content to style. #[required] diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs index 796c9ebc..3e8dba1a 100644 --- a/crates/typst-library/src/math/underover.rs +++ b/crates/typst-library/src/math/underover.rs @@ -11,14 +11,10 @@ enum LineKind { /// A horizontal line under content. /// -/// ## Example { #example } /// ```example /// $ underline(1 + 2 + ... + 5) $ /// ``` -/// -/// Display: Underline -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct UnderlineElem { /// The content above the line. #[required] @@ -34,14 +30,10 @@ impl LayoutMath for UnderlineElem { /// A horizontal line over content. /// -/// ## Example { #example } /// ```example /// $ overline(1 + 2 + ... + 5) $ /// ``` -/// -/// Display: Overline -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct OverlineElem { /// The content below the line. #[required] @@ -103,10 +95,10 @@ fn layout_underoverline( frame.push( line_pos, FrameItem::Shape( - Geometry::Line(Point::with_x(width)).stroked(Stroke { + Geometry::Line(Point::with_x(width)).stroked(FixedStroke { paint: TextElem::fill_in(ctx.styles()), thickness: bar_height, - ..Stroke::default() + ..FixedStroke::default() }), span, ), @@ -119,14 +111,10 @@ fn layout_underoverline( /// A horizontal brace under content, with an optional annotation below. /// -/// ## Example { #example } /// ```example /// $ underbrace(1 + 2 + ... + 5, "numbers") $ /// ``` -/// -/// Display: Underbrace -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct UnderbraceElem { /// The content above the brace. #[required] @@ -154,14 +142,10 @@ impl LayoutMath for UnderbraceElem { /// A horizontal brace over content, with an optional annotation above. /// -/// ## Example { #example } /// ```example /// $ overbrace(1 + 2 + ... + 5, "numbers") $ /// ``` -/// -/// Display: Overbrace -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct OverbraceElem { /// The content below the brace. #[required] @@ -189,14 +173,10 @@ impl LayoutMath for OverbraceElem { /// A horizontal bracket under content, with an optional annotation below. /// -/// ## Example { #example } /// ```example /// $ underbracket(1 + 2 + ... + 5, "numbers") $ /// ``` -/// -/// Display: Underbracket -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct UnderbracketElem { /// The content above the bracket. #[required] @@ -224,14 +204,10 @@ impl LayoutMath for UnderbracketElem { /// A horizontal bracket over content, with an optional annotation above. /// -/// ## Example { #example } /// ```example /// $ overbracket(1 + 2 + ... + 5, "numbers") $ /// ``` -/// -/// Display: Overbracket -/// Category: math -#[element(LayoutMath)] +#[elem(LayoutMath)] pub struct OverbracketElem { /// The content below the bracket. #[required] @@ -294,7 +270,7 @@ fn layout_underoverspreader( baseline = rows.len() - 1; } - let frame = stack(ctx, rows, Align::Center, gap, baseline); + let frame = stack(ctx, rows, FixedAlign::Center, gap, baseline); ctx.push(FrameFragment::new(ctx, frame).with_class(body_class)); Ok(()) @@ -307,7 +283,7 @@ fn layout_underoverspreader( pub(super) fn stack( ctx: &MathContext, rows: Vec<MathRow>, - align: Align, + align: FixedAlign, gap: Abs, baseline: usize, ) -> Frame { diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs index d871db23..4f1cc32c 100644 --- a/crates/typst-library/src/meta/bibliography.rs +++ b/crates/typst-library/src/meta/bibliography.rs @@ -30,8 +30,8 @@ use crate::text::TextElem; /// /// As soon as you add a bibliography somewhere in your document, you can start /// citing things with reference syntax (`[@key]`) or explicit calls to the -/// [citation]($func/cite) function (`[#cite("key")]`). The bibliography will -/// only show entries for works that were referenced in the document. +/// [citation]($cite) function (`[#cite("key")]`). The bibliography will only +/// show entries for works that were referenced in the document. /// /// # Example /// ```example @@ -43,10 +43,7 @@ use crate::text::TextElem; /// /// #bibliography("works.bib") /// ``` -/// -/// Display: Bibliography -/// Category: meta -#[element(Locatable, Synthesize, Show, Finalize, LocalName)] +#[elem(Locatable, Synthesize, Show, Finalize, LocalName)] pub struct BibliographyElem { /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file. #[required] @@ -78,8 +75,8 @@ pub struct BibliographyElem { /// The title of the bibliography. /// - /// - When set to `{auto}`, an appropriate title for the [text - /// language]($func/text.lang) will be used. This is the default. + /// - When set to `{auto}`, an appropriate title for the + /// [text language]($text.lang) will be used. This is the default. /// - When set to `{none}`, the bibliography will not have a title. /// - A custom title can be set by passing content. /// @@ -109,7 +106,7 @@ cast! { impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> { - let mut iter = introspector.query(&Self::func().select()).into_iter(); + let mut iter = introspector.query(&Self::elem().select()).into_iter(); let Some(elem) = iter.next() else { bail!("the document does not contain a bibliography"); }; @@ -124,7 +121,7 @@ impl BibliographyElem { /// Whether the bibliography contains the given key. pub fn has(vt: &Vt, key: &str) -> bool { vt.introspector - .query(&Self::func().select()) + .query(&Self::elem().select()) .into_iter() .flat_map(|elem| { let elem = elem.to::<Self>().unwrap(); @@ -289,8 +286,8 @@ impl BibliographyStyle { /// Cite a work from the bibliography. /// -/// Before you starting citing, you need to add a -/// [bibliography]($func/bibliography) somewhere in your document. +/// Before you starting citing, you need to add a [bibliography]($bibliography) +/// somewhere in your document. /// /// # Example /// ```example @@ -304,13 +301,10 @@ impl BibliographyStyle { /// ``` /// /// # Syntax -/// This function indirectly has dedicated syntax. [References]($func/ref) -/// can be used to cite works from the bibliography. The label then -/// corresponds to the citation key. -/// -/// Display: Citation -/// Category: meta -#[element(Locatable, Synthesize, Show)] +/// This function indirectly has dedicated syntax. [References]($ref) can be +/// used to cite works from the bibliography. The label then corresponds to the +/// citation key. +#[elem(Locatable, Synthesize, Show)] pub struct CiteElem { /// The citation keys that identify the elements that shall be cited in /// the bibliography. @@ -329,7 +323,6 @@ pub struct CiteElem { /// /// #bibliography("works.bib") /// ``` - #[positional] pub supplement: Option<Content>, /// Whether the citation should include brackets. @@ -435,8 +428,8 @@ impl Works { let citations = vt .introspector .query(&Selector::Or(eco_vec![ - RefElem::func().select(), - CiteElem::func().select(), + RefElem::elem().select(), + CiteElem::elem().select(), ])) .into_iter() .map(|elem| match elem.to::<RefElem>() { diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs index a42c6980..3a82a925 100644 --- a/crates/typst-library/src/meta/context.rs +++ b/crates/typst-library/src/meta/context.rs @@ -2,9 +2,9 @@ use crate::prelude::*; /// Provides access to the location of content. /// -/// This is useful in combination with [queries]($func/query), -/// [counters]($func/counter), [state]($func/state), and [links]($func/link). -/// See their documentation for more details. +/// This is useful in combination with [queries]($query), [counters]($counter), +/// [state]($state), and [links]($link). See their documentation for more +/// details. /// /// ```example /// #locate(loc => [ @@ -12,44 +12,10 @@ use crate::prelude::*; /// #loc.position()! /// ]) /// ``` -/// -/// ## Methods -/// ### page() -/// Returns the page number for this location. -/// -/// Note that this does not return the value of the [page counter]($func/counter) -/// at this location, but the true page number (starting from one). -/// -/// If you want to know the value of the page counter, use -/// `{counter(page).at(loc)}` instead. -/// -/// - returns: integer -/// -/// ### position() -/// Returns a dictionary with the page number and the x, y position for this -/// location. The page number starts at one and the coordinates are measured -/// from the top-left of the page. -/// -/// If you only need the page number, use `page()` instead as it allows Typst -/// to skip unnecessary work. -/// -/// - returns: dictionary -/// -/// ### page-numbering() -/// Returns the page numbering pattern of the page at this location. This can be -/// used when displaying the page counter in order to obtain the local numbering. -/// This is useful if you are building custom indices or outlines. -/// -/// If the page numbering is set to `none` at that location, this function returns `none`. -/// -/// - returns: string or function or none -/// -/// Display: Locate -/// Category: meta #[func] pub fn locate( - /// A function that receives a `location`. Its return value is displayed - /// in the document. + /// A function that receives a [`location`]($location). Its return value is + /// displayed in the document. /// /// This function is called once for each time the content returned by /// `locate` appears in the document. That makes it possible to generate @@ -60,10 +26,7 @@ pub fn locate( } /// Executes a `locate` call. -/// -/// Display: Locate -/// Category: special -#[element(Locatable, Show)] +#[elem(Locatable, Show)] struct LocateElem { /// The function to call with the location. #[required] @@ -83,9 +46,9 @@ impl Show for LocateElem { /// Provides access to active styles. /// /// The styles are currently opaque and only useful in combination with the -/// [`measure`]($func/measure) function. See its documentation for more details. -/// In the future, the provided styles might also be directly accessed to look -/// up styles defined by [set rules]($styling/#set-rules). +/// [`measure`]($measure) function. See its documentation for more details. In +/// the future, the provided styles might also be directly accessed to look up +/// styles defined by [set rules]($styling/#set-rules). /// /// ```example /// #let thing(body) = style(styles => { @@ -96,9 +59,6 @@ impl Show for LocateElem { /// #thing[Hey] \ /// #thing[Welcome] /// ``` -/// -/// Display: Style -/// Category: meta #[func] pub fn style( /// A function to call with the styles. Its return value is displayed @@ -113,10 +73,7 @@ pub fn style( } /// Executes a style access. -/// -/// Display: Style -/// Category: special -#[element(Show)] +#[elem(Show)] struct StyleElem { /// The function to call with the styles. #[required] @@ -134,10 +91,8 @@ impl Show for StyleElem { /// (width and height). /// /// The given function must accept a single parameter, `size`, which is a -/// dictionary with keys `width` and `height`, both of type -/// [`length`]($type/length). +/// dictionary with keys `width` and `height`, both of type [`length`]($length). /// - /// ```example /// #let text = lorem(30) /// #layout(size => style(styles => [ @@ -155,9 +110,9 @@ impl Show for StyleElem { /// and a height of `{400pt}`, then the specified function will be given the /// parameter `{(width: 800pt, height: 400pt)}`. If it placed directly into the /// page it receives the page's dimensions minus its margins. This is mostly -/// useful in combination with [measurement]($func/measure). +/// useful in combination with [measurement]($measure). /// -/// You can also use this function to resolve [`ratio`]($type/ratio) to fixed +/// You can also use this function to resolve [`ratio`]($ratio) to fixed /// lengths. This might come in handy if you're building your own layout /// abstractions. /// @@ -170,16 +125,13 @@ impl Show for StyleElem { /// /// Note that this function will provide an infinite width or height if one of /// the page width or height is `auto`, respectively. -/// -/// Display: Layout -/// Category: meta #[func] pub fn layout( /// A function to call with the outer container's size. Its return value is /// displayed in the document. /// - /// The container's size is given as a [dictionary]($type/dictionary) with - /// the keys `width` and `height`. + /// The container's size is given as a [dictionary]($dictionary) with the + /// keys `width` and `height`. /// /// This function is called once for each time the content returned by /// `layout` appears in the document. That makes it possible to generate @@ -190,10 +142,7 @@ pub fn layout( } /// Executes a `layout` call. -/// -/// Display: Layout -/// Category: special -#[element(Layout)] +#[elem(Layout)] struct LayoutElem { /// The function to call with the outer container's (or page's) size. #[required] diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs index 6c437469..a2a63e81 100644 --- a/crates/typst-library/src/meta/counter.rs +++ b/crates/typst-library/src/meta/counter.rs @@ -17,15 +17,14 @@ use crate::prelude::*; /// headings, figures, and more. Moreover, you can define custom counters for /// other things you want to count. /// -/// ## Displaying a counter { #displaying } +/// # Displaying a counter { #displaying } /// To display the current value of the heading counter, you call the `counter` /// function with the `key` set to `heading` and then call the `display` method /// on the counter. To see any output, you also have to enable heading -/// [numbering]($func/heading.numbering). +/// [numbering]($heading.numbering). /// /// The `display` method optionally takes an argument telling it how to format -/// the counter. This can be a [numbering pattern or a -/// function]($func/numbering). +/// the counter. This can be a [numbering pattern or a function]($numbering). /// /// ```example /// #set heading(numbering: "1.") @@ -41,7 +40,7 @@ use crate::prelude::*; /// #counter(heading).display("I") /// ``` /// -/// ## Modifying a counter { #modifying } +/// # Modifying a counter { #modifying } /// To modify a counter, you can use the `step` and `update` methods: /// /// - The `step` method increases the value of the counter by one. Because @@ -76,7 +75,6 @@ use crate::prelude::*; /// Still at #counter(heading).display(). /// ``` /// -/// ## Custom counters { #custom-counters } /// To define your own counter, call the `counter` function with a string as a /// key. This key identifies the counter globally. /// @@ -89,7 +87,7 @@ use crate::prelude::*; /// #mine.display() \ /// ``` /// -/// ## How to step { #how-to-step } +/// # How to step /// When you define and use a custom counter, in general, you should first step /// the counter and then display it. This way, the stepping behaviour of a /// counter can depend on the element it is stepped for. If you were writing a @@ -118,7 +116,7 @@ use crate::prelude::*; /// they always start at zero. This way, they are at one for the first display /// (which happens after the first step). /// -/// ## Page counter { #page-counter } +/// # Page counter /// The page counter is special. It is automatically stepped at each pagebreak. /// But like other counters, you can also step it manually. For example, you /// could have Roman page numbers for your preface, then switch to Arabic page @@ -145,7 +143,7 @@ use crate::prelude::*; /// Arabic numbers. /// ``` /// -/// ## Time travel { #time-travel } +/// # Time travel /// Counters can travel through time! You can find out the final value of the /// counter before it is reached and even determine what the value was at any /// particular location in the document. @@ -177,16 +175,16 @@ use crate::prelude::*; /// /// Let's dissect what happens in the example above: /// -/// - We call [`locate`]($func/locate) to get access to the current location in -/// the document. We then pass this location to our counter's `at` method to -/// get its value at the current location. The `at` method always returns an -/// array because counters can have multiple levels. As the counter starts at -/// zero, the first value is thus `{(0,)}`. +/// - We call [`locate`]($locate) to get access to the current location in the +/// document. We then pass this location to our counter's `at` method to get +/// its value at the current location. The `at` method always returns an array +/// because counters can have multiple levels. As the counter starts at zero, +/// the first value is thus `{(0,)}`. /// -/// - We now [`query`]($func/query) the document for all elements with the +/// - We now [`query`]($query) the document for all elements with the /// `{<intro>}` label. The result is an array from which we extract the first -/// (and only) element's [location]($type/content.location). We then look up -/// the value of the counter at that location. The first update to the counter +/// (and only) element's [location]($content.location). We then look up the +/// value of the counter at that location. The first update to the counter /// sets it to `{0 + 3 = 3}`. At the introduction heading, the value is thus /// `{(3,)}`. /// @@ -196,180 +194,31 @@ use crate::prelude::*; /// which one doesn't matter. After the heading follow two calls to `step()`, /// so the final value is `{(5,)}`. /// -/// ## Other kinds of state { #other-state } -/// The `counter` function is closely related to [state]($func/state) function. -/// Read its documentation for more details on state management in Typst and -/// why it doesn't just use normal variables for counters. -/// -/// ## Methods -/// ### display() -/// Displays the value of the counter. -/// -/// - numbering: string or function (positional) -/// A [numbering pattern or a function]($func/numbering), which specifies how -/// to display the counter. If given a function, that function receives each -/// number of the counter as a separate argument. If the amount of numbers -/// varies, e.g. for the heading argument, you can use an -/// [argument sink]($type/arguments). -/// -/// If this is omitted, displays the counter with the numbering style for the -/// counted element or with the pattern `{"1.1"}` if no such style exists. -/// -/// - both: boolean (named) -/// If enabled, displays the current and final top-level count together. Both -/// can be styled through a single numbering pattern. This is used by the page -/// numbering property to display the current and total number of pages when a -/// pattern like `{"1 / 1"}` is given. -/// -/// - returns: content -/// -/// ### step() -/// Increases the value of the counter by one. -/// -/// The update will be in effect at the position where the returned content is -/// inserted into the document. If you don't put the output into the document, -/// nothing happens! This would be the case, for example, if you write -/// `{let _ = counter(page).step()}`. Counter updates are always applied in -/// layout order and in that case, Typst wouldn't know when to step the counter. -/// -/// - level: integer (named) -/// The depth at which to step the counter. Defaults to `{1}`. -/// -/// - returns: content -/// -/// ### update() -/// Updates the value of the counter. -/// -/// Just like with `step`, the update only occurs if you put the resulting -/// content into the document. -/// -/// - value: integer or array or function (positional, required) -/// If given an integer or array of integers, sets the counter to that value. -/// If given a function, that function receives the previous counter value -/// (with each number as a separate argument) and has to return the new -/// value (integer or array). -/// -/// - returns: content -/// -/// ### at() -/// Gets the value of the counter at the given location. Always returns an -/// array of integers, even if the counter has just one number. -/// -/// - location: location (positional, required) -/// The location at which the counter value should be retrieved. A suitable -/// location can be retrieved from [`locate`]($func/locate) or -/// [`query`]($func/query). -/// -/// - returns: array -/// -/// ### final() -/// Gets the value of the counter at the end of the document. Always returns an -/// array of integers, even if the counter has just one number. -/// -/// - location: location (positional, required) -/// Can be an arbitrary location, as its value is irrelevant for the method's -/// return value. Why is it required then? Typst has to evaluate parts of your -/// code multiple times to determine all counter values. By only allowing this -/// method within [`locate`]($func/locate) calls, the amount of code that can -/// depend on the method's result is reduced. If you could call `final` -/// directly at the top level of a module, the evaluation of the whole module -/// and its exports could depend on the counter's value. -/// -/// - returns: array -/// -/// Display: Counter -/// Category: meta -#[func] -pub fn counter( - /// The key that identifies this counter. - /// - /// - If it is a string, creates a custom counter that is only affected by - /// manual updates, - /// - If this is a `{<label>}`, counts through all elements with that label, - /// - If this is an element function or selector, counts through its elements, - /// - If this is the [`page`]($func/page) function, counts through pages. - key: CounterKey, -) -> Counter { - Counter::new(key) -} - -/// Counts through pages, elements, and more. +/// # Other kinds of state { #other-state } +/// The `counter` type is closely related to [state]($state) type. Read its +/// documentation for more details on state management in Typst and why it +/// doesn't just use normal variables for counters. +#[ty(scope)] #[derive(Clone, PartialEq, Hash)] pub struct Counter(CounterKey); impl Counter { - /// Create a new counter from a key. - pub fn new(key: CounterKey) -> Self { + /// Create a new counter identified by a key. + pub fn new(key: CounterKey) -> Counter { Self(key) } /// The counter for the given element. - pub fn of(func: ElemFunc) -> Self { - Self::new(CounterKey::Selector(Selector::Elem(func, None))) - } - - /// Call a method on counter. - #[tracing::instrument(skip(vm))] - pub fn call_method( - self, - vm: &mut Vm, - method: &str, - mut args: Args, - span: Span, - ) -> SourceResult<Value> { - let value = match method { - "display" => self - .display(args.eat()?, args.named("both")?.unwrap_or(false)) - .into_value(), - "step" => self - .update(CounterUpdate::Step( - args.named("level")?.unwrap_or(NonZeroUsize::ONE), - )) - .into_value(), - "update" => self.update(args.expect("value or function")?).into_value(), - "at" => self.at(&mut vm.vt, args.expect("location")?)?.into_value(), - "final" => self.final_(&mut vm.vt, args.expect("location")?)?.into_value(), - _ => bail!(span, "type counter has no method `{}`", method), - }; - args.finish()?; - Ok(value) - } - - /// Display the current value of the counter. - pub fn display(self, numbering: Option<Numbering>, both: bool) -> Content { - DisplayElem::new(self, numbering, both).pack() - } - - /// Get the value of the state at the given location. - pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { - let sequence = self.sequence(vt)?; - let offset = vt.introspector.query(&self.selector().before(location, true)).len(); - let (mut state, page) = sequence[offset].clone(); - if self.is_page() { - let delta = vt.introspector.page(location).get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - - Ok(state) - } - - /// Get the value of the state at the final location. - pub fn final_(&self, vt: &mut Vt, _: Location) -> SourceResult<CounterState> { - let sequence = self.sequence(vt)?; - let (mut state, page) = sequence.last().unwrap().clone(); - if self.is_page() { - let delta = vt.introspector.pages().get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - Ok(state) + pub fn of(func: Element) -> Self { + Self::construct(CounterKey::Selector(Selector::Elem(func, None))) } - /// Get the current and final value of the state combined in one state. + /// Gets the current and final value of the state combined in one state. pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { let sequence = self.sequence(vt)?; let offset = vt .introspector - .query(&Selector::before(self.selector(), location, true)) + .query(&self.selector().before(location.into(), true)) .len(); let (mut at_state, at_page) = sequence[offset].clone(); let (mut final_state, final_page) = sequence.last().unwrap().clone(); @@ -384,12 +233,7 @@ impl Counter { Ok(CounterState(smallvec![at_state.first(), final_state.first()])) } - /// Produce content that performs a state update. - pub fn update(self, update: CounterUpdate) -> Content { - UpdateElem::new(self.0, update).pack() - } - - /// Produce the whole sequence of counter states. + /// Produces the whole sequence of counter states. /// /// This has to happen just once for all counters, cutting down the number /// of counter updates from quadratic to linear. @@ -462,7 +306,7 @@ impl Counter { /// The selector relevant for this counter's updates. fn selector(&self) -> Selector { let mut selector = - Selector::Elem(UpdateElem::func(), Some(dict! { "key" => self.0.clone() })); + Selector::Elem(UpdateElem::elem(), Some(dict! { "key" => self.0.clone() })); if let CounterKey::Selector(key) = &self.0 { selector = Selector::Or(eco_vec![selector, key.clone()]); @@ -477,6 +321,140 @@ impl Counter { } } +#[scope] +impl Counter { + /// Create a new counter identified by a key. + #[func(constructor)] + pub fn construct( + /// The key that identifies this counter. + /// + /// - If it is a string, creates a custom counter that is only affected + /// by manual updates, + /// - If this is a `{<label>}`, counts through all elements with that + /// label, + /// - If this is an element function or selector, counts through its + /// elements, + /// - If this is the [`page`]($page) function, counts through pages. + key: CounterKey, + ) -> Counter { + Self(key) + } + + /// Displays the current value of the counter. + #[func] + pub fn display( + self, + /// A [numbering pattern or a function]($numbering), which specifies how + /// to display the counter. If given a function, that function receives + /// each number of the counter as a separate argument. If the amount of + /// numbers varies, e.g. for the heading argument, you can use an + /// [argument sink]($arguments). + /// + /// If this is omitted, displays the counter with the numbering style + /// for the counted element or with the pattern `{"1.1"}` if no such + /// style exists. + #[default] + numbering: Option<Numbering>, + /// If enabled, displays the current and final top-level count together. + /// Both can be styled through a single numbering pattern. This is used + /// by the page numbering property to display the current and total + /// number of pages when a pattern like `{"1 / 1"}` is given. + #[named] + #[default(false)] + both: bool, + ) -> Content { + DisplayElem::new(self, numbering, both).pack() + } + + /// Increases the value of the counter by one. + /// + /// The update will be in effect at the position where the returned content + /// is inserted into the document. If you don't put the output into the + /// document, nothing happens! This would be the case, for example, if you + /// write `{let _ = counter(page).step()}`. Counter updates are always + /// applied in layout order and in that case, Typst wouldn't know when to + /// step the counter. + #[func] + pub fn step( + self, + /// The depth at which to step the counter. Defaults to `{1}`. + #[named] + #[default(NonZeroUsize::ONE)] + level: NonZeroUsize, + ) -> Content { + self.update(CounterUpdate::Step(level)) + } + + /// Updates the value of the counter. + /// + /// Just like with `step`, the update only occurs if you put the resulting + /// content into the document. + #[func] + pub fn update( + self, + /// If given an integer or array of integers, sets the counter to that + /// value. If given a function, that function receives the previous + /// counter value (with each number as a separate argument) and has to + /// return the new value (integer or array). + update: CounterUpdate, + ) -> Content { + UpdateElem::new(self.0, update).pack() + } + + /// Gets the value of the counter at the given location. Always returns an + /// array of integers, even if the counter has just one number. + #[func] + pub fn at( + &self, + /// The virtual typesetter. + vt: &mut Vt, + /// The location at which the counter value should be retrieved. A + /// suitable location can be retrieved from [`locate`]($locate) or + /// [`query`]($query). + location: Location, + ) -> SourceResult<CounterState> { + let sequence = self.sequence(vt)?; + let offset = vt + .introspector + .query(&self.selector().before(location.into(), true)) + .len(); + let (mut state, page) = sequence[offset].clone(); + if self.is_page() { + let delta = vt.introspector.page(location).get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + + Ok(state) + } + + /// Gets the value of the counter at the end of the document. Always returns + /// an array of integers, even if the counter has just one number. + #[func] + pub fn final_( + &self, + /// The virtual typesetter. + vt: &mut Vt, + /// Can be an arbitrary location, as its value is irrelevant for the + /// method's return value. Why is it required then? Typst has to + /// evaluate parts of your code multiple times to determine all counter + /// values. By only allowing this method within [`locate`]($locate) + /// calls, the amount of code that can depend on the method's result is + /// reduced. If you could call `final` directly at the top level of a + /// module, the evaluation of the whole module and its exports could + /// depend on the counter's value. + location: Location, + ) -> SourceResult<CounterState> { + let _ = location; + let sequence = self.sequence(vt)?; + let (mut state, page) = sequence.last().unwrap().clone(); + if self.is_page() { + let delta = vt.introspector.pages().get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) + } +} + impl Debug for Counter { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("counter(")?; @@ -486,7 +464,7 @@ impl Debug for Counter { } cast! { - type Counter: "counter", + type Counter, } /// Identifies a counter. @@ -504,14 +482,14 @@ pub enum CounterKey { cast! { CounterKey, self => match self { - Self::Page => PageElem::func().into_value(), + Self::Page => PageElem::elem().into_value(), Self::Selector(v) => v.into_value(), Self::Str(v) => v.into_value(), }, v: Str => Self::Str(v), v: Label => Self::Selector(Selector::Label(v)), - v: ElemFunc => { - if v == PageElem::func() { + v: Element => { + if v == PageElem::elem() { Self::Page } else { Self::Selector(LocatableSelector::from_value(v.into_value())?.0) @@ -531,6 +509,7 @@ impl Debug for CounterKey { } /// An update to perform on a counter. +#[ty] #[derive(Clone, PartialEq, Hash)] pub enum CounterUpdate { /// Set the counter to the specified state. @@ -548,7 +527,7 @@ impl Debug for CounterUpdate { } cast! { - type CounterUpdate: "counter update", + type CounterUpdate, v: CounterState => Self::Set(v), v: Func => Self::Func(v), } @@ -612,10 +591,7 @@ cast! { } /// Executes a display of a state. -/// -/// Display: State -/// Category: special -#[element(Locatable, Show)] +#[elem(Locatable, Show)] struct DisplayElem { /// The counter. #[required] @@ -643,11 +619,11 @@ impl Show for DisplayElem { return None; }; - if func == HeadingElem::func() { + if func == HeadingElem::elem() { HeadingElem::numbering_in(styles) - } else if func == FigureElem::func() { + } else if func == FigureElem::elem() { FigureElem::numbering_in(styles) - } else if func == EquationElem::func() { + } else if func == EquationElem::elem() { EquationElem::numbering_in(styles) } else { None @@ -667,10 +643,7 @@ impl Show for DisplayElem { } /// Executes a display of a state. -/// -/// Display: State -/// Category: special -#[element(Locatable, Show)] +#[elem(Locatable, Show)] struct UpdateElem { /// The key that identifies the counter. #[required] diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs index db036e0a..66f8aeb5 100644 --- a/crates/typst-library/src/meta/document.rs +++ b/crates/typst-library/src/meta/document.rs @@ -17,10 +17,7 @@ use crate::prelude::*; /// /// Note that metadata set with this function is not rendered within the /// document. Instead, it is embedded in the compiled PDF file. -/// -/// Display: Document -/// Category: meta -#[element(Construct, LayoutRoot)] +#[elem(Construct, LayoutRoot)] pub struct DocumentElem { /// The document's title. This is often rendered as the title of the /// PDF viewer window. diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs index d63ae3a8..6e95dce7 100644 --- a/crates/typst-library/src/meta/figure.rs +++ b/crates/typst-library/src/meta/figure.rs @@ -15,7 +15,7 @@ use crate::visualize::ImageElem; /// For example, figures containing images will be numbered separately from /// figures containing tables. /// -/// ## Examples { #examples } +/// # Examples /// The example below shows a basic figure with an image: /// ```example /// @glacier shows a glacier. Glaciers @@ -27,9 +27,8 @@ use crate::visualize::ImageElem; /// ) <glacier> /// ``` /// -/// You can also insert [tables]($func/table) into figures to give them a -/// caption. The figure will detect this and automatically use a separate -/// counter. +/// You can also insert [tables]($table) into figures to give them a caption. +/// The figure will detect this and automatically use a separate counter. /// /// ```example /// #figure( @@ -45,7 +44,7 @@ use crate::visualize::ImageElem; /// This behaviour can be overridden by explicitly specifying the figure's /// `kind`. All figures of the same kind share a common counter. /// -/// ## Modifying the appearance { #modifying-appearance } +/// # Modifying the appearance { #modifying-appearance } /// You can completely customize the look of your figures with a [show /// rule]($styling/#show-rules). In the example below, we show the figure's /// caption above its body and display its supplement and counter after the @@ -73,13 +72,10 @@ use crate::visualize::ImageElem; /// If your figure is too large and its contents are breakable across pages /// (e.g. if it contains a large table), then you can make the figure breakable /// across pages as well by using `[#show figure: set block(breakable: true)]` -/// (see the [block]($func/block) documentation for more information). -/// -/// Display: Figure -/// Category: meta -#[element(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] +/// (see the [block]($block) documentation for more information). +#[elem(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] pub struct FigureElem { - /// The content of the figure. Often, an [image]($func/image). + /// The content of the figure. Often, an [image]($image). #[required] pub body: Content, @@ -103,7 +99,7 @@ pub struct FigureElem { /// ) /// #lorem(60) /// ``` - pub placement: Option<Smart<VerticalAlign>>, + pub placement: Option<Smart<VAlign>>, /// The figure's caption. pub caption: Option<Content>, @@ -122,8 +118,17 @@ pub struct FigureElem { /// caption: [I'm down here], /// ) /// ``` - #[default(VerticalAlign(GenAlign::Specific(Align::Bottom)))] - pub caption_pos: VerticalAlign, + #[default(VAlign::Bottom)] + #[parse({ + let option: Option<Spanned<VAlign>> = args.named("caption-pos")?; + if let Some(Spanned { v: align, span }) = option { + if align == VAlign::Horizon { + bail!(span, "expected `top` or `bottom`"); + } + } + option.map(|spanned| spanned.v) + })] + pub caption_pos: VAlign, /// The kind of figure this is. /// @@ -133,7 +138,7 @@ pub struct FigureElem { /// Setting this to something other than `{auto}` will override the /// automatic detection. This can be useful if /// - you wish to create a custom figure type that is not an - /// [image]($func/image), a [table]($func/table) or [code]($func/raw), + /// [image]($image), a [table]($table) or [code]($raw), /// - you want to force the figure to use a specific counter regardless of /// its content. /// @@ -155,8 +160,8 @@ pub struct FigureElem { /// The figure's supplement. /// /// If set to `{auto}`, the figure will try to automatically determine the - /// correct supplement based on the `kind` and the active [text - /// language]($func/text.lang). If you are using a custom figure type, you + /// correct supplement based on the `kind` and the active + /// [text language]($text.lang). If you are using a custom figure type, you /// will need to manually specify the supplement. /// /// If a function is specified, it is passed the first descendant of the @@ -174,7 +179,7 @@ pub struct FigureElem { pub supplement: Smart<Option<Supplement>>, /// How to number the figure. Accepts a - /// [numbering pattern or function]($func/numbering). + /// [numbering pattern or function]($numbering). #[default(Some(NumberingPattern::from_str("1").unwrap().into()))] pub numbering: Option<Numbering>, @@ -182,16 +187,15 @@ pub struct FigureElem { #[default(Em::new(0.65).into())] pub gap: Length, - /// Whether the figure should appear in an [`outline`]($func/outline) - /// of figures. + /// Whether the figure should appear in an [`outline`]($outline) of figures. #[default(true)] pub outlined: bool, /// Convenience field to get access to the counter for this figure. /// /// The counter only depends on the `kind`: - /// - For (tables)[$func/table]: `{counter(figure.where(kind: table))}` - /// - For (images)[$func/image]: `{counter(figure.where(kind: image))}` + /// - For (tables)[@table]: `{counter(figure.where(kind: table))}` + /// - For (images)[@image]: `{counter(figure.where(kind: image))}` /// - For a custom kind: `{counter(figure.where(kind: kind))}` /// /// These are the counters you'll need to modify if you want to skip a @@ -210,16 +214,9 @@ impl Synthesize for FigureElem { .query_first(Selector::can::<dyn Figurable>()) .cloned() .map(|elem| FigureKind::Elem(elem.func())) - .unwrap_or_else(|| FigureKind::Elem(ImageElem::func())) + .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) }); - let caption_pos = - VerticalAlign(GenAlign::Specific(match self.caption_pos(styles) { - VerticalAlign(GenAlign::Specific(Align::Top)) => Align::Top, - VerticalAlign(GenAlign::Specific(Align::Bottom)) => Align::Bottom, - _ => bail!(self.span(), "caption-pos can only be top or bottom"), - })); - // Resolve the supplement. let supplement = match self.supplement(styles) { Smart::Auto => { @@ -261,14 +258,14 @@ impl Synthesize for FigureElem { // Construct the figure's counter. let counter = Counter::new(CounterKey::Selector(Selector::Elem( - Self::func(), + Self::elem(), Some(dict! { "kind" => kind.clone(), }), ))); self.push_placement(self.placement(styles)); - self.push_caption_pos(caption_pos); + self.push_caption_pos(self.caption_pos(styles)); self.push_caption(self.caption(styles)); self.push_kind(Smart::Custom(kind)); self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); @@ -288,10 +285,7 @@ impl Show for FigureElem { // Build the caption, if any. if let Some(caption) = self.full_caption(vt)? { let v = VElem::weak(self.gap(styles).into()).pack(); - realized = if matches!( - self.caption_pos(styles), - VerticalAlign(GenAlign::Specific(Align::Bottom)) - ) { + realized = if self.caption_pos(styles) == VAlign::Bottom { realized + v + caption } else { caption + v + realized @@ -302,15 +296,13 @@ impl Show for FigureElem { realized = BlockElem::new() .with_body(Some(realized)) .pack() - .aligned(Axes::with_x(Some(Align::Center.into()))); + .aligned(Align::CENTER); // Wrap in a float. if let Some(align) = self.placement(styles) { realized = PlaceElem::new(realized) .with_float(true) - .with_alignment(align.map(|VerticalAlign(align)| { - Axes::new(Some(Align::Center.into()), Some(align)) - })) + .with_alignment(align.map(|align| HAlign::Center + align)) .pack(); } @@ -345,7 +337,7 @@ impl Refable for FigureElem { } fn counter(&self) -> Counter { - self.counter().unwrap_or_else(|| Counter::of(Self::func())) + self.counter().unwrap_or_else(|| Counter::of(Self::elem())) } fn numbering(&self) -> Option<Numbering> { @@ -379,8 +371,8 @@ impl FigureElem { self.counter(), self.numbering(StyleChain::default()), ) { - let loc = self.0.location().unwrap(); - let numbers = counter.at(vt, loc)?.display(vt, &numbering)?; + let location = self.0.location().unwrap(); + let numbers = counter.at(vt, location)?.display(vt, &numbering)?; if !supplement.is_empty() { supplement += TextElem::packed("\u{a0}"); @@ -397,7 +389,7 @@ impl FigureElem { #[derive(Debug, Clone)] pub enum FigureKind { /// The kind is an element function. - Elem(ElemFunc), + Elem(Element), /// The kind is a name. Name(EcoString), } @@ -408,7 +400,7 @@ cast! { Self::Elem(v) => v.into_value(), Self::Name(v) => v.into_value(), }, - v: ElemFunc => Self::Elem(v), + v: Element => Self::Elem(v), v: EcoString => Self::Name(v), } diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs index 848c0b7c..ed7242bb 100644 --- a/crates/typst-library/src/meta/footnote.rs +++ b/crates/typst-library/src/meta/footnote.rs @@ -34,11 +34,11 @@ cast! { /// and can break across multiple pages. /// /// To customize the appearance of the entry in the footnote listing, see -/// [`footnote.entry`]($func/footnote.entry). The footnote itself is realized as -/// a normal superscript, so you can use a set rule on the -/// [`super`]($func/super) function to customize it. +/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a +/// normal superscript, so you can use a set rule on the [`super`]($super) +/// function to customize it. /// -/// ## Example { #example } +/// # Example /// ```example /// Check the docs for more details. /// #footnote[https://typst.app/docs] @@ -46,7 +46,7 @@ cast! { /// /// The footnote automatically attaches itself to the preceding word, even if /// there is a space before it in the markup. To force space, you can use the -/// string `[#" "]` or explicit [horizontal spacing]($func/h). +/// string `[#" "]` or explicit [horizontal spacing]($h). /// /// By giving a label to a footnote, you can have multiple references to it. /// @@ -61,21 +61,14 @@ cast! { /// apply to the footnote's content. See [here][issue] for more information. /// /// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 -/// -/// Display: Footnote -/// Category: meta -#[element(Locatable, Synthesize, Show, Count)] -#[scope( - scope.define("entry", FootnoteEntry::func()); - scope -)] +#[elem(scope, Locatable, Synthesize, Show, Count)] pub struct FootnoteElem { /// How to number footnotes. /// /// By default, the footnote numbering continues throughout your document. /// If you prefer per-page footnote numbering, you can reset the footnote - /// [counter]($func/counter) in the page [header]($func/page.header). In the - /// future, there might be a simpler way to achieve this. + /// [counter]($counter) in the page [header]($page.header). In the future, + /// there might be a simpler way to achieve this. /// /// ```example /// #set footnote(numbering: "*") @@ -93,6 +86,12 @@ pub struct FootnoteElem { pub body: FootnoteBody, } +#[scope] +impl FootnoteElem { + #[elem] + type FootnoteEntry; +} + impl FootnoteElem { /// Creates a new footnote that the passed content as its body. pub fn with_content(content: Content) -> Self { @@ -145,7 +144,7 @@ impl Show for FootnoteElem { Ok(vt.delayed(|vt| { let loc = self.declaration_location(vt).at(self.span())?; let numbering = self.numbering(styles); - let counter = Counter::of(Self::func()); + let counter = Counter::of(Self::elem()); let num = counter.at(vt, loc)?.display(vt, &numbering)?; let sup = SuperElem::new(num).pack(); let hole = HElem::new(Abs::zero().into()).with_weak(true).pack(); @@ -168,11 +167,9 @@ impl Count for FootnoteElem { /// /// _Note:_ Set and show rules for `footnote.entry` must be defined at the /// beginning of the document in order to work correctly. -/// See [here][issue] for more information. -/// -/// [issue]: https://github.com/typst/typst/issues/1348#issuecomment-1566316463 +/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463) +/// for more information. /// -/// ## Example { #example } /// ```example /// #show footnote.entry: set text(red) /// @@ -180,10 +177,7 @@ impl Count for FootnoteElem { /// #footnote[It's down here] /// has red text! /// ``` -/// -/// Display: Footnote Entry -/// Category: meta -#[element(Show, Finalize)] +#[elem(name = "entry", title = "Footnote Entry", Show, Finalize)] pub struct FootnoteEntry { /// The footnote for this entry. It's location can be used to determine /// the footnote counter state. @@ -220,7 +214,7 @@ pub struct FootnoteEntry { #[default( LineElem::new() .with_length(Ratio::new(0.3).into()) - .with_stroke(PartialStroke { + .with_stroke(Stroke { thickness: Smart::Custom(Abs::pt(0.5).into()), ..Default::default() }) @@ -273,7 +267,7 @@ impl Show for FootnoteEntry { let note = self.note(); let number_gap = Em::new(0.05); let numbering = note.numbering(StyleChain::default()); - let counter = Counter::of(FootnoteElem::func()); + let counter = Counter::of(FootnoteElem::elem()); let loc = note.0.location().unwrap(); let num = counter.at(vt, loc)?.display(vt, &numbering)?; let sup = SuperElem::new(num) diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs index 84da7c03..edd2d0f0 100644 --- a/crates/typst-library/src/meta/heading.rs +++ b/crates/typst-library/src/meta/heading.rs @@ -17,14 +17,13 @@ use crate::text::{SpaceElem, TextElem, TextSize}; /// /// Typst can automatically number your headings for you. To enable numbering, /// specify how you want your headings to be numbered with a -/// [numbering pattern or function]($func/numbering). +/// [numbering pattern or function]($numbering). /// /// Independently from the numbering, Typst can also automatically generate an -/// [outline]($func/outline) of all headings for you. To exclude one or more -/// headings from this outline, you can set the `outlined` parameter to -/// `{false}`. +/// [outline]($outline) of all headings for you. To exclude one or more headings +/// from this outline, you can set the `outlined` parameter to `{false}`. /// -/// ## Example { #example } +/// # Example /// ```example /// #set heading(numbering: "1.a)") /// @@ -35,21 +34,18 @@ use crate::text::{SpaceElem, TextElem, TextSize}; /// To start, ... /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// Headings have dedicated syntax: They can be created by starting a line with /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. -/// -/// Display: Heading -/// Category: meta -#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)] pub struct HeadingElem { /// The logical nesting depth of the heading, starting from one. #[default(NonZeroUsize::ONE)] pub level: NonZeroUsize, /// How to number the heading. Accepts a - /// [numbering pattern or function]($func/numbering). + /// [numbering pattern or function]($numbering). /// /// ```example /// #set heading(numbering: "1.a.") @@ -78,11 +74,11 @@ pub struct HeadingElem { /// ``` pub supplement: Smart<Option<Supplement>>, - /// Whether the heading should appear in the [outline]($func/outline). + /// Whether the heading should appear in the [outline]($outline). /// - /// Note that this property, if set to `{true}`, ensures the heading is - /// also shown as a bookmark in the exported PDF's outline (when exporting - /// to PDF). To change that behavior, use the `bookmarked` property. + /// Note that this property, if set to `{true}`, ensures the heading is also + /// shown as a bookmark in the exported PDF's outline (when exporting to + /// PDF). To change that behavior, use the `bookmarked` property. /// /// ```example /// #outline() @@ -103,9 +99,8 @@ pub struct HeadingElem { /// The default value of `{auto}` indicates that the heading will only /// appear in the exported PDF's outline if its `outlined` property is set /// to `{true}`, that is, if it would also be listed in Typst's - /// [outline]($func/outline). Setting this property to either - /// `{true}` (bookmark) or `{false}` (don't bookmark) bypasses that - /// behavior. + /// [outline]($outline). Setting this property to either `{true}` (bookmark) + /// or `{false}` (don't bookmark) bypasses that behavior. /// /// ```example /// #heading[Normal heading] @@ -149,7 +144,7 @@ impl Show for HeadingElem { fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let mut realized = self.body(); if let Some(numbering) = self.numbering(styles) { - realized = Counter::of(Self::func()) + realized = Counter::of(Self::elem()) .display(Some(numbering), false) .spanned(self.span()) + HElem::new(Em::new(0.3).into()).with_weak(true).pack() @@ -205,7 +200,7 @@ impl Refable for HeadingElem { } fn counter(&self) -> Counter { - Counter::of(Self::func()) + Counter::of(Self::elem()) } fn numbering(&self) -> Option<Numbering> { @@ -221,7 +216,7 @@ impl Outlinable for HeadingElem { let mut content = self.body(); if let Some(numbering) = self.numbering(StyleChain::default()) { - let numbers = Counter::of(Self::func()) + let numbers = Counter::of(Self::elem()) .at(vt, self.0.location().unwrap())? .display(vt, &numbering)?; content = numbers + SpaceElem::new().pack() + content; diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs index 2a53b84f..7b68b186 100644 --- a/crates/typst-library/src/meta/link.rs +++ b/crates/typst-library/src/meta/link.rs @@ -6,7 +6,7 @@ use crate::text::{Hyphenate, TextElem}; /// By default, links are not styled any different from normal text. However, /// you can easily apply a style of your choice with a show rule. /// -/// ## Example { #example } +/// # Example /// ```example /// #show link: underline /// @@ -18,13 +18,10 @@ use crate::text::{Hyphenate, TextElem}; /// ] /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. -/// -/// Display: Link -/// Category: meta -#[element(Show)] +#[elem(Show)] pub struct LinkElem { /// The destination the link points to. /// @@ -35,17 +32,16 @@ pub struct LinkElem { /// /// - To link to another part of the document, `dest` can take one of three /// forms: - /// - A [label]($func/label) attached to an element. If you also want - /// automatic text for the link based on the element, consider using - /// a [reference]($func/ref) instead. + /// - A [label]($label) attached to an element. If you also want automatic + /// text for the link based on the element, consider using a + /// [reference]($ref) instead. /// - /// - A [location]($func/locate) resulting from a [`locate`]($func/locate) - /// call or [`query`]($func/query). + /// - A [location]($locate) resulting from a [`locate`]($locate) call or + /// [`query`]($query). /// - /// - A dictionary with a `page` key of type [integer]($type/integer) and - /// `x` and `y` coordinates of type [length]($type/length). Pages are - /// counted from one, and the coordinates are relative to the page's top - /// left corner. + /// - A dictionary with a `page` key of type [integer]($int) and `x` and + /// `y` coordinates of type [length]($length). Pages are counted from + /// one, and the coordinates are relative to the page's top left corner. /// /// ```example /// = Introduction <intro> diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst-library/src/meta/metadata.rs index 3e08a9f7..b4ae64cb 100644 --- a/crates/typst-library/src/meta/metadata.rs +++ b/crates/typst-library/src/meta/metadata.rs @@ -2,11 +2,11 @@ use crate::prelude::*; /// Exposes a value to the query system without producing visible content. /// -/// This element can be retrieved with the [`query`]($func/query) function and -/// from the command with [`typst query`]($reference/meta/query/#cli-queries). -/// Its purpose is to expose an arbitrary value to the introspection system. To -/// identify a metadata value among others, you can attach a -/// [`label`]($func/label) to it and query for that label. +/// This element can be retrieved with the [`query`]($query) function and from +/// the command with [`typst query`]($reference/meta/query/#cli-queries). Its +/// purpose is to expose an arbitrary value to the introspection system. To +/// identify a metadata value among others, you can attach a [`label`]($label) +/// to it and query for that label. /// /// The `metadata` element is especially useful for command line queries because /// it allows you to expose arbitrary values to the outside world. @@ -20,10 +20,7 @@ use crate::prelude::*; /// query(<note>, loc).first().value /// }) /// ``` -/// -/// Display: Metadata -/// Category: meta -#[element(Behave, Show, Locatable)] +#[elem(Behave, Show, Locatable)] pub struct MetadataElem { /// The value to embed into the document. #[required] diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs index bb6ac3d3..659cb5a3 100644 --- a/crates/typst-library/src/meta/mod.rs +++ b/crates/typst-library/src/meta/mod.rs @@ -9,9 +9,11 @@ mod footnote; mod heading; mod link; mod metadata; -mod numbering; +#[path = "numbering.rs"] +mod numbering_; mod outline; -mod query; +#[path = "query.rs"] +mod query_; mod reference; mod state; @@ -24,9 +26,9 @@ pub use self::footnote::*; pub use self::heading::*; pub use self::link::*; pub use self::metadata::*; -pub use self::numbering::*; +pub use self::numbering_::*; pub use self::outline::*; -pub use self::query::*; +pub use self::query_::*; pub use self::reference::*; pub use self::state::*; @@ -35,24 +37,27 @@ use crate::text::TextElem; /// Hook up all meta definitions. pub(super) fn define(global: &mut Scope) { - global.define("document", DocumentElem::func()); - global.define("ref", RefElem::func()); - global.define("link", LinkElem::func()); - global.define("outline", OutlineElem::func()); - global.define("heading", HeadingElem::func()); - global.define("figure", FigureElem::func()); - global.define("footnote", FootnoteElem::func()); - global.define("cite", CiteElem::func()); - global.define("bibliography", BibliographyElem::func()); - global.define("locate", locate_func()); - global.define("style", style_func()); - global.define("layout", layout_func()); - global.define("counter", counter_func()); - global.define("numbering", numbering_func()); - global.define("state", state_func()); - global.define("query", query_func()); - global.define("selector", selector_func()); - global.define("metadata", MetadataElem::func()); + global.category("meta"); + global.define_type::<Label>(); + global.define_type::<Selector>(); + global.define_type::<Location>(); + global.define_type::<Counter>(); + global.define_type::<State>(); + global.define_elem::<DocumentElem>(); + global.define_elem::<RefElem>(); + global.define_elem::<LinkElem>(); + global.define_elem::<OutlineElem>(); + global.define_elem::<HeadingElem>(); + global.define_elem::<FigureElem>(); + global.define_elem::<FootnoteElem>(); + global.define_elem::<CiteElem>(); + global.define_elem::<BibliographyElem>(); + global.define_elem::<MetadataElem>(); + global.define_func::<locate>(); + global.define_func::<style>(); + global.define_func::<layout>(); + global.define_func::<numbering>(); + global.define_func::<query>(); } /// The named with which an element is referenced. diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs index 8698f7b9..40308af0 100644 --- a/crates/typst-library/src/meta/numbering.rs +++ b/crates/typst-library/src/meta/numbering.rs @@ -16,7 +16,7 @@ use crate::text::Case; /// number is substituted, their prefixes, and one suffix. The prefixes and the /// suffix are repeated as-is. /// -/// ## Example { #example } +/// # Example /// ```example /// #numbering("1.1)", 1, 2, 3) \ /// #numbering("1.a.i", 1, 2) \ @@ -29,11 +29,10 @@ use crate::text::Case; /// 1, 2, 3, /// ) /// ``` -/// -/// Display: Numbering -/// Category: meta #[func] pub fn numbering( + /// The virtual machine. + vm: &mut Vm, /// Defines how the numbering works. /// /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`, @@ -64,8 +63,6 @@ pub fn numbering( /// given, the last counting symbol with its prefix is repeated. #[variadic] numbers: Vec<usize>, - /// The virtual machine. - vm: &mut Vm, ) -> SourceResult<Value> { numbering.apply_vm(vm, &numbers) } diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs index 6fd4ebd0..8583d96f 100644 --- a/crates/typst-library/src/meta/outline.rs +++ b/crates/typst-library/src/meta/outline.rs @@ -16,7 +16,7 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// be displayed in the outline alongside its title or caption. By default this /// generates a table of contents. /// -/// ## Example { #example } +/// # Example /// ```example /// #outline() /// @@ -27,13 +27,13 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// #lorem(10) /// ``` /// -/// ## Alternative outlines { #alternative-outlines } +/// # Alternative outlines /// By setting the `target` parameter, the outline can be used to generate a /// list of other kinds of elements than headings. In the example below, we list /// all figures containing images by setting `target` to `{figure.where(kind: /// image)}`. We could have also set it to just `figure`, but then the list /// would also include figures containing tables or other material. For more -/// details on the `where` selector, [see here]($type/content.where). +/// details on the `where` selector, [see here]($function.where). /// /// ```example /// #outline( @@ -47,25 +47,17 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// ) /// ``` /// -/// ## Styling the outline { #styling-the-outline } +/// # Styling the outline /// The outline element has several options for customization, such as its -/// `title` and `indent` parameters. If desired, however, it is possible to -/// have more control over the outline's look and style through the -/// [`outline.entry`]($func/outline.entry) element. -/// -/// Display: Outline -/// Category: meta -/// Keywords: Table of Contents -#[element(Show, Finalize, LocalName)] -#[scope( - scope.define("entry", OutlineEntry::func()); - scope -)] +/// `title` and `indent` parameters. If desired, however, it is possible to have +/// more control over the outline's look and style through the +/// [`outline.entry`]($outline.entry) element. +#[elem(scope, keywords = ["Table of Contents"], Show, Finalize, LocalName)] pub struct OutlineElem { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the - /// [text language]($func/text.lang) will be used. This is the default. + /// [text language]($text.lang) will be used. This is the default. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. /// @@ -97,7 +89,7 @@ pub struct OutlineElem { /// ) /// ``` #[default(LocatableSelector(Selector::Elem( - HeadingElem::func(), + HeadingElem::elem(), Some(dict! { "outlined" => true }) )))] pub target: LocatableSelector, @@ -125,19 +117,18 @@ pub struct OutlineElem { /// - `{none}`: No indent /// - `{auto}`: Indents the numbering of the nested entry with the title of /// its parent entry. This only has an effect if the entries are numbered - /// (e.g., via [heading numbering]($func/heading.numbering)). - /// - [Relative length]($type/relative-length): Indents the item by this length + /// (e.g., via [heading numbering]($heading.numbering)). + /// - [Relative length]($relative): Indents the item by this length /// multiplied by its nesting level. Specifying `{2em}`, for instance, /// would indent top-level headings (not nested) by `{0em}`, second level /// headings by `{2em}` (nested once), third-level headings by `{4em}` /// (nested twice) and so on. - /// - [Function]($type/function): You can completely customize this setting - /// with a function. That function receives the nesting level as a - /// parameter (starting at 0 for top-level headings/elements) and can - /// return a relative length or content making up the indent. For example, - /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, - /// while `{n => [→ ] * n}` would indent with one arrow per nesting - /// level. + /// - [Function]($function): You can completely customize this setting with + /// a function. That function receives the nesting level as a parameter + /// (starting at 0 for top-level headings/elements) and can return a + /// relative length or content making up the indent. For example, + /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while + /// `{n => [→ ] * n}` would indent with one arrow per nesting level. /// /// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or /// `{false}` (equivalent to `{none}`) for this option is deprecated and @@ -184,6 +175,12 @@ pub struct OutlineElem { pub fill: Option<Content>, } +#[scope] +impl OutlineElem { + #[elem] + type OutlineEntry; +} + impl Show for OutlineElem { #[tracing::instrument(name = "OutlineElem::show", skip_all)] fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { @@ -391,11 +388,9 @@ cast! { /// outlined element, its page number, and the filler content between both. /// /// This element is intended for use with show rules to control the appearance -/// of outlines. -/// -/// ## Example { #example } -/// The example below shows how to style entries for top-level sections to make -/// them stand out. +/// of outlines. To customize an entry's line, you can build it from scratch by +/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the +/// entry. /// /// ```example /// #set heading(numbering: "1.") @@ -416,13 +411,7 @@ cast! { /// = Analysis /// == Setup /// ``` -/// -/// To completely customize an entry's line, you can also build it from scratch -/// by accessing the `level`, `element`, `body`, `fill` and `page` fields on the entry. -/// -/// Display: Outline Entry -/// Category: meta -#[element(Show)] +#[elem(name = "entry", title = "Outline Entry", Show)] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -430,8 +419,8 @@ pub struct OutlineEntry { pub level: NonZeroUsize, /// The element this entry refers to. Its location will be available - /// through the [`location`]($type/content.location) method on content - /// and can be [linked]($func/link) to. + /// through the [`location`]($content.location) method on content + /// and can be [linked]($link) to. #[required] pub element: Content, @@ -446,7 +435,7 @@ pub struct OutlineEntry { /// located in. When `{none}`, empty space is inserted in that gap instead. /// /// Note that, when using show rules to override outline entries, it is - /// recommended to wrap the filling content in a [`box`]($func/box) with + /// recommended to wrap the filling content in a [`box`]($box) with /// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show /// precisely as many `-` characters as necessary to fill a particular gap. #[required] diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs index eb520896..d6c600d7 100644 --- a/crates/typst-library/src/meta/query.rs +++ b/crates/typst-library/src/meta/query.rs @@ -4,10 +4,10 @@ use crate::prelude::*; /// /// The `query` functions lets you search your document for elements of a /// particular type or with a particular label. To use it, you first need to -/// retrieve the current document location with the [`locate`]($func/locate) +/// retrieve the current document location with the [`locate`]($locate) /// function. /// -/// ## Finding elements { #finding-elements } +/// # Finding elements /// In the example below, we create a custom page header that displays the text /// "Typst Academy" in small capitals and the current section title. On the /// first page, the section title is omitted because the header is before the @@ -59,7 +59,7 @@ use crate::prelude::*; /// #lorem(15) /// ``` /// -/// ## A word of caution { #caution } +/// # A word of caution { #caution } /// To resolve all your queries, Typst evaluates and layouts parts of the /// document multiple times. However, there is no guarantee that your queries /// can actually be completely resolved. If you aren't careful a query can @@ -73,9 +73,9 @@ use crate::prelude::*; /// on. As we can see, the output has five headings. This is because Typst /// simply gives up after five attempts. /// -/// In general, you should try not to write queries that affect themselves. -/// The same words of caution also apply to other introspection features like -/// [counters]($func/counter) and [state]($func/state). +/// In general, you should try not to write queries that affect themselves. The +/// same words of caution also apply to other introspection features like +/// [counters]($counter) and [state]($state). /// /// ```example /// = Real @@ -86,11 +86,11 @@ use crate::prelude::*; /// }) /// ``` /// -/// ## Command line queries { #command-line-queries } +/// # Command line queries /// You can also perform queries from the command line with the `typst query` /// command. This command executes an arbitrary query on the document and /// returns the resulting elements in serialized form. Consider the following -/// `example.typ` file which contains some invisible [metadata]($func/metadata): +/// `example.typ` file which contains some invisible [metadata]($metadata): /// /// ```typ /// #metadata("This is a note") <note> @@ -125,32 +125,29 @@ use crate::prelude::*; /// $ typst query example.typ "<note>" --field value --one /// "This is a note" /// ``` -/// -/// Display: Query -/// Category: meta #[func] pub fn query( + /// The virtual machine. + vm: &mut Vm, /// Can be an element function like a `heading` or `figure`, a `{<label>}` /// or a more complex selector like `{heading.where(level: 1)}`. /// /// Currently, only a subset of element functions is supported. Aside from /// headings and figures, this includes equations, references and all /// elements with an explicit label. As a result, you _can_ query for e.g. - /// [`strong`]($func/strong) elements, but you will find only those that - /// have an explicit label attached to them. This limitation will be - /// resolved in the future. + /// [`strong`]($strong) elements, but you will find only those that have an + /// explicit label attached to them. This limitation will be resolved in the + /// future. target: LocatableSelector, /// Can be an arbitrary location, as its value is irrelevant for the /// function's return value. Why is it required then? As noted before, Typst /// has to evaluate parts of your code multiple times to determine the /// values of all state. By only allowing this function within - /// [`locate`]($func/locate) calls, the amount of code that can depend on - /// the query's result is reduced. If you could call it directly at the top + /// [`locate`]($locate) calls, the amount of code that can depend on the + /// query's result is reduced. If you could call it directly at the top /// level of a module, the evaluation of the whole module and its exports /// could depend on the query's result. location: Location, - /// The virtual machine. - vm: &mut Vm, ) -> Array { let _ = location; let vec = vm.vt.introspector.query(&target.0); @@ -158,19 +155,3 @@ pub fn query( .map(|elem| Value::Content(elem.into_inner())) .collect() } - -/// Turns a value into a selector. The following values are accepted: -/// - An element function like a `heading` or `figure`. -/// - A `{<label>}`. -/// - A more complex selector like `{heading.where(level: 1)}`. -/// -/// Display: Selector -/// Category: meta -#[func] -pub fn selector( - /// Can be an element function like a `heading` or `figure`, a `{<label>}` - /// or a more complex selector like `{heading.where(level: 1)}`. - target: Selector, -) -> Selector { - target -} diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs index 015157a8..7f05bfcc 100644 --- a/crates/typst-library/src/meta/reference.rs +++ b/crates/typst-library/src/meta/reference.rs @@ -9,20 +9,19 @@ use crate::text::TextElem; /// Produces a textual reference to a label. For example, a reference to a /// heading will yield an appropriate string such as "Section 1" for a reference /// to the first heading. The references are also links to the respective -/// element. Reference syntax can also be used to [cite]($func/cite) from a +/// element. Reference syntax can also be used to [cite]($cite) from a /// bibliography. /// -/// Referenceable elements include [headings]($func/heading), -/// [figures]($func/figure), [equations]($func/math.equation), and -/// [footnotes]($func/footnote). To create a custom referenceable element like a -/// theorem, you can create a figure of a custom [`kind`]($func/figure.kind) and -/// write a show rule for it. In the future, there might be a more direct way to -/// define a custom referenceable element. +/// Referenceable elements include [headings]($heading), [figures]($figure), +/// [equations]($math.equation), and [footnotes]($footnote). To create a custom +/// referenceable element like a theorem, you can create a figure of a custom +/// [`kind`]($figure.kind) and write a show rule for it. In the future, there +/// might be a more direct way to define a custom referenceable element. /// /// If you just want to link to a labelled element and not get an automatic -/// textual reference, consider using the [`link`]($func/link) function instead. +/// textual reference, consider using the [`link`]($link) function instead. /// -/// ## Example { #example } +/// # Example /// ```example /// #set heading(numbering: "1.") /// #set math.equation(numbering: "(1)") @@ -46,7 +45,7 @@ use crate::text::TextElem; /// #bibliography("works.bib") /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: A reference to a label can be /// created by typing an `@` followed by the name of the label (e.g. /// `[= Introduction <intro>]` can be referenced by typing `[@intro]`). @@ -54,7 +53,7 @@ use crate::text::TextElem; /// To customize the supplement, add content in square brackets after the /// reference: `[@intro[Chapter]]`. /// -/// ## Customization { #customization } +/// # Customization /// If you write a show rule for references, you can access the referenced /// element through the `element` field of the reference. The `element` may /// be `{none}` even if it exists if Typst hasn't discovered it yet, so you @@ -83,10 +82,7 @@ use crate::text::TextElem; /// In @beginning we prove @pythagoras. /// $ a^2 + b^2 = c^2 $ <pythagoras> /// ``` -/// -/// Display: Reference -/// Category: meta -#[element(Synthesize, Locatable, Show)] +#[elem(title = "Reference", Synthesize, Locatable, Show)] pub struct RefElem { /// The target label that should be referenced. #[required] @@ -163,7 +159,7 @@ impl Show for RefElem { let elem = elem.at(span)?; - if elem.func() == FootnoteElem::func() { + if elem.func() == FootnoteElem::elem() { return Ok(FootnoteElem::with_label(target).pack().spanned(span)); } @@ -192,7 +188,7 @@ impl Show for RefElem { .hint(eco_format!( "you can enable {} numbering with `#set {}(numbering: \"1.\")`", elem.func().name(), - if elem.func() == EquationElem::func() { + if elem.func() == EquationElem::elem() { "math.equation" } else { elem.func().name() diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs index ee2a6e32..f7b5eb77 100644 --- a/crates/typst-library/src/meta/state.rs +++ b/crates/typst-library/src/meta/state.rs @@ -30,7 +30,7 @@ use crate::prelude::*; /// #compute("x - 5") /// ``` /// -/// ## State and document markup { #state-and-markup } +/// # State and document markup { #state-and-markup } /// Why does it do that? Because, in general, this kind of computation with side /// effects is problematic in document markup and Typst is upfront about that. /// For the results to make sense, the computation must proceed in the same @@ -61,7 +61,7 @@ use crate::prelude::*; /// `template` function and only then sees the `Outline`. Just counting up would /// number the `Introduction` with `1` and the `Outline` with `2`. /// -/// ## Managing state in Typst { #state-in-typst } +/// # Managing state in Typst { #state-in-typst } /// So what do we do instead? We use Typst's state management system. Calling /// the `state` function with an identifying string key and an optional initial /// value gives you a state value which exposes a few methods. The two most @@ -122,14 +122,14 @@ use crate::prelude::*; /// /// This example is of course a bit silly, but in practice this is often exactly /// what you want! A good example are heading counters, which is why Typst's -/// [counting system]($func/counter) is very similar to its state system. +/// [counting system]($counter) is very similar to its state system. /// -/// ## Time Travel { #time-travel } +/// # Time Travel /// By using Typst's state management system you also get time travel -/// capabilities! By combining the state system with [`locate`]($func/locate) -/// and [`query`]($func/query), we can find out what the value of the state will -/// be at any position in the document from anywhere else. In particular, the -/// `at` method gives us the value of the state at any location and the `final` +/// capabilities! By combining the state system with [`locate`]($locate) and +/// [`query`]($query), we can find out what the value of the state will be at +/// any position in the document from anywhere else. In particular, the `at` +/// method gives us the value of the state at any location and the `final` /// methods gives us the value of the state at the end of the document. /// /// ```example @@ -156,7 +156,7 @@ use crate::prelude::*; /// #compute("x - 5") /// ``` /// -/// ## A word of caution { #caution } +/// # A word of caution { #caution } /// To resolve the values of all states, Typst evaluates parts of your code /// multiple times. However, there is no guarantee that your state manipulation /// can actually be completely resolved. @@ -180,73 +180,7 @@ use crate::prelude::*; /// `locate` calls or `display` calls of state or counters. Instead, pass a /// function to `update` that determines the value of the state based on its /// previous value. -/// -/// ## Methods -/// ### display() -/// Displays the value of the state. -/// -/// - format: function (positional) -/// A function which receives the value of the state and can return arbitrary -/// content which is then displayed. If this is omitted, the value is directly -/// displayed. -/// -/// - returns: content -/// -/// ### update() -/// Updates the value of the state. -/// -/// The update will be in effect at the position where the returned content is -/// inserted into the document. If you don't put the output into the document, -/// nothing happens! This would be the case, for example, if you write -/// `{let _ = state("key").update(7)}`. State updates are always applied in -/// layout order and in that case, Typst wouldn't know when to update the state. -/// -/// - value: any or function (positional, required) -/// If given a non function-value, sets the state to that value. If given a -/// function, that function receives the previous state and has to return the -/// new state. -/// -/// - returns: content -/// -/// ### at() -/// Gets the value of the state at the given location. -/// -/// - location: location (positional, required) -/// The location at which the state's value should be retrieved. A suitable -/// location can be retrieved from [`locate`]($func/locate) or -/// [`query`]($func/query). -/// -/// - returns: any -/// -/// ### final() -/// Gets the value of the state at the end of the document. -/// -/// - location: location (positional, required) -/// Can be an arbitrary location, as its value is irrelevant for the method's -/// return value. Why is it required then? As noted before, Typst has to -/// evaluate parts of your code multiple times to determine the values of all -/// state. By only allowing this method within [`locate`]($func/locate) calls, -/// the amount of code that can depend on the method's result is reduced. If -/// you could call `final` directly at the top level of a module, the -/// evaluation of the whole module and its exports could depend on the state's -/// value. -/// -/// - returns: any -/// -/// Display: State -/// Category: meta -#[func] -pub fn state( - /// The key that identifies this state. - key: Str, - /// The initial value of the state. - #[default] - init: Value, -) -> State { - State { key, init } -} - -/// A state. +#[ty(scope)] #[derive(Clone, PartialEq, Hash)] pub struct State { /// The key that identifies the state. @@ -256,49 +190,9 @@ pub struct State { } impl State { - /// Call a method on a state. - #[tracing::instrument(skip(vm))] - pub fn call_method( - self, - vm: &mut Vm, - method: &str, - mut args: Args, - span: Span, - ) -> SourceResult<Value> { - let value = match method { - "display" => self.display(args.eat()?).into_value(), - "at" => self.at(&mut vm.vt, args.expect("location")?)?, - "final" => self.final_(&mut vm.vt, args.expect("location")?)?, - "update" => self.update(args.expect("value or function")?).into_value(), - _ => bail!(span, "type state has no method `{}`", method), - }; - args.finish()?; - Ok(value) - } - - /// Display the current value of the state. - pub fn display(self, func: Option<Func>) -> Content { - DisplayElem::new(self, func).pack() - } - - /// Get the value of the state at the given location. - #[tracing::instrument(skip(self, vt))] - pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> { - let sequence = self.sequence(vt)?; - let offset = vt.introspector.query(&self.selector().before(location, true)).len(); - Ok(sequence[offset].clone()) - } - - /// Get the value of the state at the final location. - #[tracing::instrument(skip(self, vt))] - pub fn final_(self, vt: &mut Vt, _: Location) -> SourceResult<Value> { - let sequence = self.sequence(vt)?; - Ok(sequence.last().unwrap().clone()) - } - - /// Produce content that performs a state update. - pub fn update(self, update: StateUpdate) -> Content { - UpdateElem::new(self.key, update).pack() + /// Create a new state identified by a key. + pub fn new(key: Str, init: Value) -> State { + Self { key, init } } /// Produce the whole sequence of states. @@ -350,7 +244,94 @@ impl State { /// The selector for this state's updates. fn selector(&self) -> Selector { - Selector::Elem(UpdateElem::func(), Some(dict! { "key" => self.key.clone() })) + Selector::Elem(UpdateElem::elem(), Some(dict! { "key" => self.key.clone() })) + } +} + +#[scope] +impl State { + /// Create a new state identified by a key. + #[func(constructor)] + pub fn construct( + /// The key that identifies this state. + key: Str, + /// The initial value of the state. + #[default] + init: Value, + ) -> State { + Self::new(key, init) + } + + /// Displays the current value of the state. + #[func] + pub fn display( + self, + /// A function which receives the value of the state and can return + /// arbitrary content which is then displayed. If this is omitted, the + /// value is directly displayed. + #[default] + func: Option<Func>, + ) -> Content { + DisplayElem::new(self, func).pack() + } + + /// Update the value of the state. + /// + /// The update will be in effect at the position where the returned content + /// is inserted into the document. If you don't put the output into the + /// document, nothing happens! This would be the case, for example, if you + /// write `{let _ = state("key").update(7)}`. State updates are always + /// applied in layout order and in that case, Typst wouldn't know when to + /// update the state. + #[func] + pub fn update( + self, + /// If given a non function-value, sets the state to that value. If + /// given a function, that function receives the previous state and has + /// to return the new state. + update: StateUpdate, + ) -> Content { + UpdateElem::new(self.key, update).pack() + } + + /// Get the value of the state at the given location. + #[func] + pub fn at( + &self, + /// The virtual typesetter. + vt: &mut Vt, + /// The location at which the state's value should be retrieved. A + /// suitable location can be retrieved from [`locate`]($locate) or + /// [`query`]($query). + location: Location, + ) -> SourceResult<Value> { + let sequence = self.sequence(vt)?; + let offset = vt + .introspector + .query(&self.selector().before(location.into(), true)) + .len(); + Ok(sequence[offset].clone()) + } + + /// Get the value of the state at the end of the document. + #[func] + pub fn final_( + &self, + /// The virtual typesetter. + vt: &mut Vt, + /// Can be an arbitrary location, as its value is irrelevant for the + /// method's return value. Why is it required then? As noted before, + /// Typst has to evaluate parts of your code multiple times to determine + /// the values of all state. By only allowing this method within + /// [`locate`]($locate) calls, the amount of code that can depend on the + /// method's result is reduced. If you could call `final` directly at + /// the top level of a module, the evaluation of the whole module and + /// its exports could depend on the state's value. + location: Location, + ) -> SourceResult<Value> { + let _ = location; + let sequence = self.sequence(vt)?; + Ok(sequence.last().unwrap().clone()) } } @@ -365,10 +346,11 @@ impl Debug for State { } cast! { - type State: "state", + type State, } /// An update to perform on a state. +#[ty] #[derive(Clone, PartialEq, Hash)] pub enum StateUpdate { /// Set the state to the specified value. @@ -384,16 +366,13 @@ impl Debug for StateUpdate { } cast! { - type StateUpdate: "state update", + type StateUpdate, v: Func => Self::Func(v), v: Value => Self::Set(v), } /// Executes a display of a state. -/// -/// Display: State -/// Category: special -#[element(Locatable, Show)] +#[elem(Locatable, Show)] struct DisplayElem { /// The state. #[required] @@ -419,10 +398,7 @@ impl Show for DisplayElem { } /// Executes a display of a state. -/// -/// Display: State -/// Category: special -#[element(Locatable, Show)] +#[elem(Locatable, Show)] struct UpdateElem { /// The key that identifies the state. #[required] diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs index b4acbea0..aca5a064 100644 --- a/crates/typst-library/src/prelude.rs +++ b/crates/typst-library/src/prelude.rs @@ -15,15 +15,15 @@ pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult}; pub use typst::doc::*; #[doc(no_inline)] pub use typst::eval::{ - array, cast, dict, format_str, func, Args, Array, AutoValue, Cast, Dict, FromValue, - Func, IntoValue, Never, NoneValue, Scope, Str, Symbol, Type, Value, Vm, + array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict, + FromValue, Func, IntoValue, Scope, Str, Symbol, Type, Value, Vm, }; #[doc(no_inline)] pub use typst::geom::*; #[doc(no_inline)] pub use typst::model::{ - element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold, - Introspector, Label, Locatable, LocatableSelector, Location, Locator, MetaElem, + elem, Behave, Behaviour, Construct, Content, Element, Finalize, Fold, Introspector, + Label, Locatable, LocatableSelector, Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show, StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt, }; diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs index d7c80a30..e6431423 100644 --- a/crates/typst-library/src/shared/ext.rs +++ b/crates/typst-library/src/shared/ext.rs @@ -24,7 +24,7 @@ pub trait ContentExt { fn backlinked(self, loc: Location) -> Self; /// Set alignments for this content. - fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self; + fn aligned(self, align: Align) -> Self; /// Pad this content at the sides. fn padded(self, padding: Sides<Rel<Length>>) -> Self; @@ -56,8 +56,8 @@ impl ContentExt for Content { self.styled(MetaElem::set_data(vec![Meta::Elem(backlink)])) } - fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self { - self.styled(AlignElem::set_alignment(aligns)) + fn aligned(self, align: Align) -> Self { + self.styled(AlignElem::set_alignment(align)) } fn padded(self, padding: Sides<Rel<Length>>) -> Self { diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs index 44bc3e14..b35dfcaa 100644 --- a/crates/typst-library/src/symbols/emoji.rs +++ b/crates/typst-library/src/symbols/emoji.rs @@ -6,7 +6,7 @@ pub fn emoji() -> Module { for (name, symbol) in EMOJI { scope.define(*name, symbol.clone()); } - Module::new("emoji").with_scope(scope) + Module::new("emoji", scope) } /// A list of named emoji. diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs index 5036aa11..0d288c3b 100644 --- a/crates/typst-library/src/symbols/mod.rs +++ b/crates/typst-library/src/symbols/mod.rs @@ -10,6 +10,8 @@ use crate::prelude::*; /// Hook up all symbol definitions. pub(super) fn define(global: &mut Scope) { - global.define("sym", sym()); - global.define("emoji", emoji()); + global.category("symbols"); + global.define_type::<Symbol>(); + global.define_module(sym()); + global.define_module(emoji()); } diff --git a/crates/typst-library/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs index a9ec2485..bfc22ea4 100644 --- a/crates/typst-library/src/symbols/sym.rs +++ b/crates/typst-library/src/symbols/sym.rs @@ -6,7 +6,7 @@ pub fn sym() -> Module { for (name, symbol) in SYM { scope.define(*name, symbol.clone()); } - Module::new("sym").with_scope(scope) + Module::new("sym", scope) } /// The list of general symbols. diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index c97ef325..e34bf363 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -6,19 +6,15 @@ use crate::prelude::*; /// Underlines text. /// -/// ## Example { #example } +/// # Example /// ```example /// This is #underline[important]. /// ``` -/// -/// Display: Underline -/// Category: text -#[element(Show)] +#[elem(Show)] pub struct UnderlineElem { - /// How to stroke the line. + /// How to [stroke]($stroke) the line. /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in + /// If set to `{auto}`, takes on the text's color and a thickness defined in /// the current font. /// /// ```example @@ -30,7 +26,7 @@ pub struct UnderlineElem { /// ``` #[resolve] #[fold] - pub stroke: Smart<PartialStroke>, + pub stroke: Smart<Stroke>, /// The position of the line relative to the baseline, read from the font /// tables if `{auto}`. @@ -85,19 +81,15 @@ impl Show for UnderlineElem { /// Adds a line over text. /// -/// ## Example { #example } +/// # Example /// ```example /// #overline[A line over text.] /// ``` -/// -/// Display: Overline -/// Category: text -#[element(Show)] +#[elem(Show)] pub struct OverlineElem { - /// How to stroke the line. + /// How to [stroke]($stroke) the line. /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in + /// If set to `{auto}`, takes on the text's color and a thickness defined in /// the current font. /// /// ```example @@ -110,7 +102,7 @@ pub struct OverlineElem { /// ``` #[resolve] #[fold] - pub stroke: Smart<PartialStroke>, + pub stroke: Smart<Stroke>, /// The position of the line relative to the baseline. Read from the font /// tables if `{auto}`. @@ -170,23 +162,19 @@ impl Show for OverlineElem { /// Strikes through text. /// -/// ## Example { #example } +/// # Example /// ```example /// This is #strike[not] relevant. /// ``` -/// -/// Display: Strikethrough -/// Category: text -#[element(Show)] +#[elem(title = "Strikethrough", Show)] pub struct StrikeElem { - /// How to stroke the line. + /// How to [stroke]($stroke) the line. /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in + /// If set to `{auto}`, takes on the text's color and a thickness defined in /// the current font. /// - /// _Note:_ Please don't use this for real redaction as you can still - /// copy paste the text. + /// _Note:_ Please don't use this for real redaction as you can still copy + /// paste the text. /// /// ```example /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \ @@ -194,7 +182,7 @@ pub struct StrikeElem { /// ``` #[resolve] #[fold] - pub stroke: Smart<PartialStroke>, + pub stroke: Smart<Stroke>, /// The position of the line relative to the baseline. Read from the font /// tables if `{auto}`. @@ -240,14 +228,11 @@ impl Show for StrikeElem { /// Highlights text with a background color. /// -/// ## Example { #example } +/// # Example /// ```example /// This is #highlight[important]. /// ``` -/// -/// Display: Highlight -/// Category: text -#[element(Show)] +#[elem(Show)] pub struct HighlightElem { /// The color to highlight the text with. /// (Default: 0xffff5f) @@ -316,6 +301,7 @@ impl Show for HighlightElem { /// Defines a line-based decoration that is positioned over, under or on top of text, /// or highlights the text with a background. +#[ty] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Decoration { line: DecoLine, @@ -332,15 +318,15 @@ impl Fold for Decoration { } cast! { - type Decoration: "decoration", + type Decoration, } /// A kind of decorative line. #[derive(Debug, Clone, Eq, PartialEq, Hash)] enum DecoLine { - Underline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool }, - Strikethrough { stroke: PartialStroke<Abs>, offset: Smart<Abs> }, - Overline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool }, + Underline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool }, + Strikethrough { stroke: Stroke<Abs>, offset: Smart<Abs> }, + Overline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool }, Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge }, } @@ -378,10 +364,10 @@ pub(super) fn decorate( }; let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = stroke.clone().unwrap_or(Stroke { + let stroke = stroke.clone().unwrap_or(FixedStroke { paint: text.fill.clone(), thickness: metrics.thickness.at(text.size), - ..Stroke::default() + ..FixedStroke::default() }); let gap_padding = 0.08 * text.size; diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs index 811b027e..73657345 100644 --- a/crates/typst-library/src/text/misc.rs +++ b/crates/typst-library/src/text/misc.rs @@ -2,10 +2,7 @@ use super::TextElem; use crate::prelude::*; /// A text space. -/// -/// Display: Space -/// Category: text -#[element(Behave, Unlabellable, PlainText)] +#[elem(Behave, Unlabellable, PlainText)] pub struct SpaceElem {} impl Behave for SpaceElem { @@ -28,21 +25,18 @@ impl PlainText for SpaceElem { /// end of a paragraph is ignored, but more than one creates additional empty /// lines. /// -/// ## Example { #example } +/// # Example /// ```example /// *Date:* 26.12.2022 \ /// *Topic:* Infrastructure Test \ /// *Severity:* High \ /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: To insert a line break, simply write /// a backslash followed by whitespace. This always creates an unjustified /// break. -/// -/// Display: Line Break -/// Category: text -#[element(Behave)] +#[elem(title = "Line Break", Behave)] pub struct LinebreakElem { /// Whether to justify the line before the break. /// @@ -71,7 +65,7 @@ impl Behave for LinebreakElem { /// /// Increases the current font weight by a given `delta`. /// -/// ## Example { #example } +/// # Example /// ```example /// This is *strong.* \ /// This is #strong[too.] \ @@ -80,15 +74,12 @@ impl Behave for LinebreakElem { /// And this is *evermore.* /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: To strongly emphasize content, /// simply enclose it in stars/asterisks (`*`). Note that this only works at /// word boundaries. To strongly emphasize part of a word, you have to use the /// function. -/// -/// Display: Strong Emphasis -/// Category: text -#[element(Show)] +#[elem(title = "Strong Emphasis", Show)] pub struct StrongElem { /// The delta to apply on the font weight. /// @@ -131,12 +122,12 @@ impl Fold for Delta { /// Emphasizes content by setting it in italics. /// -/// - If the current [text style]($func/text.style) is `{"normal"}`, -/// this turns it into `{"italic"}`. -/// - If it is already `{"italic"}` or `{"oblique"}`, -/// it turns it back to `{"normal"}`. +/// - If the current [text style]($text.style) is `{"normal"}`, this turns it +/// into `{"italic"}`. +/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to +/// `{"normal"}`. /// -/// ## Example { #example } +/// # Example /// ```example /// This is _emphasized._ \ /// This is #emph[too.] @@ -148,14 +139,11 @@ impl Fold for Delta { /// This is _emphasized_ differently. /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: To emphasize content, simply /// enclose it in underscores (`_`). Note that this only works at word /// boundaries. To emphasize part of a word, you have to use the function. -/// -/// Display: Emphasis -/// Category: text -#[element(Show)] +#[elem(title = "Emphasis", Show)] pub struct EmphElem { /// The content to emphasize. #[required] @@ -189,16 +177,13 @@ impl Fold for Toggle { /// Converts text or content to lowercase. /// -/// ## Example { #example } +/// # Example /// ```example /// #lower("ABC") \ /// #lower[*My Text*] \ /// #lower[already low] /// ``` -/// -/// Display: Lowercase -/// Category: text -#[func] +#[func(title = "Lowercase")] pub fn lower( /// The text to convert to lowercase. text: Caseable, @@ -208,16 +193,13 @@ pub fn lower( /// Converts text or content to uppercase. /// -/// ## Example { #example } +/// # Example /// ```example /// #upper("abc") \ /// #upper[*my text*] \ /// #upper[ALREADY HIGH] /// ``` -/// -/// Display: Uppercase -/// Category: text -#[func] +#[func(title = "Uppercase")] pub fn upper( /// The text to convert to uppercase. text: Caseable, @@ -278,7 +260,7 @@ impl Case { /// support selecting a dedicated smallcaps font as well as synthesizing /// smallcaps from normal letters, but this is not yet implemented. /// -/// ## Example { #example } +/// # Example /// ```example /// #set par(justify: true) /// #set heading(numbering: "I.") @@ -292,10 +274,7 @@ impl Case { /// = Introduction /// #lorem(40) /// ``` -/// -/// Display: Small Capitals -/// Category: text -#[func] +#[func(title = "Small Capitals")] pub fn smallcaps( /// The text to display to small capitals. body: Content, @@ -310,7 +289,7 @@ pub fn smallcaps( /// the same but randomly chosen. As usual for blind texts, it does not make any /// sense. Use it as a placeholder to try layouts. /// -/// ## Example { #example } +/// # Example /// ```example /// = Blind Text /// #lorem(30) @@ -318,10 +297,7 @@ pub fn smallcaps( /// = More Blind Text /// #lorem(15) /// ``` -/// -/// Display: Blind Text -/// Category: text -#[func] +#[func(keywords = ["Blind Text"])] pub fn lorem( /// The length of the blind text in words. words: usize, diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 4f3c1591..8bce5e8a 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -23,22 +23,23 @@ use crate::prelude::*; /// Hook up all text definitions. pub(super) fn define(global: &mut Scope) { - global.define("text", TextElem::func()); - global.define("linebreak", LinebreakElem::func()); - global.define("smartquote", SmartQuoteElem::func()); - global.define("strong", StrongElem::func()); - global.define("emph", EmphElem::func()); - global.define("lower", lower_func()); - global.define("upper", upper_func()); - global.define("smallcaps", smallcaps_func()); - global.define("sub", SubElem::func()); - global.define("super", SuperElem::func()); - global.define("underline", UnderlineElem::func()); - global.define("strike", StrikeElem::func()); - global.define("highlight", HighlightElem::func()); - global.define("overline", OverlineElem::func()); - global.define("raw", RawElem::func()); - global.define("lorem", lorem_func()); + global.category("text"); + global.define_elem::<TextElem>(); + global.define_elem::<LinebreakElem>(); + global.define_elem::<SmartquoteElem>(); + global.define_elem::<StrongElem>(); + global.define_elem::<EmphElem>(); + global.define_elem::<SubElem>(); + global.define_elem::<SuperElem>(); + global.define_elem::<UnderlineElem>(); + global.define_elem::<OverlineElem>(); + global.define_elem::<StrikeElem>(); + global.define_elem::<HighlightElem>(); + global.define_elem::<RawElem>(); + global.define_func::<lower>(); + global.define_func::<upper>(); + global.define_func::<smallcaps>(); + global.define_func::<lorem>(); } /// Customizes the look and layout of text in a variety of ways. @@ -47,7 +48,7 @@ pub(super) fn define(global: &mut Scope) { /// the set rule is often the simpler choice, calling the `text` function /// directly can be useful when passing text as an argument to another function. /// -/// ## Example { #example } +/// # Example /// ```example /// #set text(18pt) /// With a set rule. @@ -56,10 +57,7 @@ pub(super) fn define(global: &mut Scope) { /// With a function call. /// ]) /// ``` -/// -/// Display: Text -/// Category: text -#[element(Construct, PlainText)] +#[elem(Construct, PlainText)] pub struct TextElem { /// A prioritized sequence of font families. /// @@ -111,8 +109,8 @@ pub struct TextElem { /// italic and oblique style is rarely observable. /// /// If you want to emphasize your text, you should do so using the - /// [emph]($func/emph) function instead. This makes it easy to adapt the - /// style later if you change your mind about how to signify the emphasis. + /// [emph]($emph) function instead. This makes it easy to adapt the style + /// later if you change your mind about how to signify the emphasis. /// /// ```example /// #text(font: "Linux Libertine", style: "italic")[Italic] @@ -126,7 +124,7 @@ pub struct TextElem { /// that is closest in weight. /// /// If you want to strongly emphasize your text, you should do so using the - /// [strong]($func/strong) function instead. This makes it easy to adapt the + /// [strong]($strong) function instead. This makes it easy to adapt the /// style later if you change your mind about how to signify the strong /// emphasis. /// @@ -147,7 +145,7 @@ pub struct TextElem { /// the text if a condensed or expanded version of the font is available. /// /// If you want to adjust the amount of space between characters instead of - /// stretching the glyphs itself, use the [`tracking`]($func/text.tracking) + /// stretching the glyphs itself, use the [`tracking`]($text.tracking) /// property instead. /// /// ```example @@ -196,7 +194,7 @@ pub struct TextElem { /// the space character in the font. /// /// If you want to adjust the amount of space between characters rather than - /// words, use the [`tracking`]($func/text.tracking) property instead. + /// words, use the [`tracking`]($text.tracking) property instead. /// /// ```example /// #set text(spacing: 200%) @@ -272,7 +270,7 @@ pub struct TextElem { /// /// - The text processing pipeline can make more informed choices. /// - Hyphenation will use the correct patterns for the language. - /// - [Smart quotes]($func/smartquote) turns into the correct quotes for the + /// - [Smart quotes]($smartquote) turns into the correct quotes for the /// language. /// - And all other things which are language-aware. /// @@ -327,13 +325,13 @@ pub struct TextElem { /// - `{rtl}`: Layout text from right to left. /// /// When writing in right-to-left scripts like Arabic or Hebrew, you should - /// set the [text language]($func/text.lang) or direction. While individual - /// runs of text are automatically layouted in the correct direction, - /// setting the dominant direction gives the bidirectional reordering - /// algorithm the necessary information to correctly place punctuation and - /// inline objects. Furthermore, setting the direction affects the alignment - /// values `start` and `end`, which are equivalent to `left` and `right` in - /// `ltr` text and the other way around in `rtl` text. + /// set the [text language]($text.lang) or direction. While individual runs + /// of text are automatically layouted in the correct direction, setting the + /// dominant direction gives the bidirectional reordering algorithm the + /// necessary information to correctly place punctuation and inline objects. + /// Furthermore, setting the direction affects the alignment values `start` + /// and `end`, which are equivalent to `left` and `right` in `ltr` text and + /// the other way around in `rtl` text. /// /// If you set this to `rtl` and experience bugs or in some way bad looking /// output, please do get in touch with us through the @@ -350,7 +348,7 @@ pub struct TextElem { /// Whether to hyphenate text to improve line breaking. When `{auto}`, text /// will be hyphenated if and only if justification is enabled. /// - /// Setting the [text language]($func/text.lang) ensures that the correct + /// Setting the [text language]($text.lang) ensures that the correct /// hyphenation patterns are used. /// /// ```example diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst-library/src/text/quotes.rs index cf4a03d5..a47f7ed5 100644 --- a/crates/typst-library/src/text/quotes.rs +++ b/crates/typst-library/src/text/quotes.rs @@ -5,9 +5,9 @@ use crate::prelude::*; /// A language-aware quote that reacts to its context. /// /// Automatically turns into an appropriate opening or closing quote based on -/// the active [text language]($func/text.lang). +/// the active [text language]($text.lang). /// -/// ## Example { #example } +/// # Example /// ```example /// "This is in quotes." /// @@ -18,14 +18,11 @@ use crate::prelude::*; /// "C'est entre guillemets." /// ``` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax: The normal quote characters /// (`'` and `"`). Typst automatically makes your quotes smart. -/// -/// Display: Smart Quote -/// Category: text -#[element] -pub struct SmartQuoteElem { +#[elem] +pub struct SmartquoteElem { /// Whether this should be a double quote. #[default(true)] pub double: bool, diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index a5699afd..1f46f94d 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -12,7 +12,7 @@ use typst::util::option_eq; use unicode_segmentation::UnicodeSegmentation; use super::{ - FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize, + FontFamily, FontList, Hyphenate, LinebreakElem, SmartquoteElem, TextElem, TextSize, }; use crate::layout::BlockElem; use crate::meta::{Figurable, LocalName}; @@ -23,7 +23,7 @@ use crate::prelude::*; /// Displays the text verbatim and in a monospace font. This is typically used /// to embed computer code into your document. /// -/// ## Example { #example } +/// # Example /// ````example /// Adding `rbx` to `rcx` gives /// the desired result. @@ -43,7 +43,7 @@ use crate::prelude::*; /// also trimmed. /// ```` /// -/// ## Syntax { #syntax } +/// # Syntax /// This function also has dedicated syntax. You can enclose text in 1 or 3+ /// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text. /// When you use three or more backticks, you can additionally specify a @@ -57,10 +57,15 @@ use crate::prelude::*; /// needed, start the text with a single space (which will be trimmed) or use /// the single backtick syntax. If your text should start or end with a /// backtick, put a space before or after it (it will be trimmed). -/// -/// Display: Raw Text / Code -/// Category: text -#[element(Synthesize, Show, Finalize, LocalName, Figurable, PlainText)] +#[elem( + title = "Raw Text / Code", + Synthesize, + Show, + Finalize, + LocalName, + Figurable, + PlainText +)] pub struct RawElem { /// The raw text. /// @@ -153,8 +158,8 @@ pub struct RawElem { /// code = "centered" /// ``` /// ```` - #[default(HorizontalAlign(GenAlign::Start))] - pub align: HorizontalAlign, + #[default(HAlign::Start)] + pub align: HAlign, /// One or multiple additional syntax definitions to load. The syntax /// definitions should be in the @@ -190,10 +195,10 @@ pub struct RawElem { /// Applying a theme only affects the color of specifically highlighted /// text. It does not consider the theme's foreground and background /// properties, so that you retain control over the color of raw text. You - /// can apply the foreground color yourself with the [`text`]($func/text) - /// function and the background with a [filled block]($func/block.fill). You - /// could also use the [`xml`]($func/xml) function to extract these - /// properties from the theme. + /// can apply the foreground color yourself with the [`text`]($text) + /// function and the background with a [filled block]($block.fill). You + /// could also use the [`xml`]($xml) function to extract these properties + /// from the theme. /// /// ````example /// #set raw(theme: "halcyon.tmTheme") @@ -340,7 +345,7 @@ impl Show for RawElem { if self.block(styles) { // Align the text before inserting it into the block. - realized = realized.aligned(Axes::with_x(Some(self.align(styles).into()))); + realized = realized.aligned(self.align(styles).into()); realized = BlockElem::new().with_body(Some(realized)).pack(); } @@ -356,7 +361,7 @@ impl Finalize for RawElem { styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); styles .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - styles.set(SmartQuoteElem::set_enabled(false)); + styles.set(SmartquoteElem::set_enabled(false)); realized.styled_with_map(styles) } } diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 65e309e1..6cb4d895 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -5,14 +5,11 @@ use crate::prelude::*; /// /// The text is rendered smaller and its baseline is lowered. /// -/// ## Example { #example } +/// # Example /// ```example /// Revenue#sub[yearly] /// ``` -/// -/// Display: Subscript -/// Category: text -#[element(Show)] +#[elem(title = "Subscript", Show)] pub struct SubElem { /// Whether to prefer the dedicated subscript characters of the font. /// @@ -68,14 +65,11 @@ impl Show for SubElem { /// /// The text is rendered smaller and its baseline is raised. /// -/// ## Example { #example } +/// # Example /// ```example /// 1#super[st] try! /// ``` -/// -/// Display: Superscript -/// Category: text -#[element(Show)] +#[elem(title = "Superscript", Show)] pub struct SuperElem { /// Whether to prefer the dedicated superscript characters of the font. /// diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs index a06509dd..e6269198 100644 --- a/crates/typst-library/src/visualize/image.rs +++ b/crates/typst-library/src/visualize/image.rs @@ -18,7 +18,7 @@ use crate::text::families; /// in the resulting PDF. Make sure to double-check embedded SVG images. If you /// have an issue, also feel free to report it on [GitHub][gh-svg]. /// -/// ## Example { #example } +/// # Example /// ```example /// #figure( /// image("molecular.jpg", width: 80%), @@ -30,14 +30,7 @@ use crate::text::families; /// ``` /// /// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg -/// -/// Display: Image -/// Category: visualize -#[element(Layout, LocalName, Figurable)] -#[scope( - scope.define("decode", image_decode_func()); - scope -)] +#[elem(scope, Layout, LocalName, Figurable)] pub struct ImageElem { /// Path to an image file. #[required] @@ -73,59 +66,58 @@ pub struct ImageElem { pub fit: ImageFit, } -/// Decode a raster or vector graphic from bytes or a string. -/// -/// ## Example { #example } -/// ```example -/// #let original = read("diagram.svg") -/// #let changed = original.replace( -/// "#2B80FF", // blue -/// green.hex(), -/// ) -/// -/// #image.decode(original) -/// #image.decode(changed) -/// ``` -/// -/// Display: Decode Image -/// Category: visualize -#[func] -pub fn image_decode( - /// The data to decode as an image. Can be a string for SVGs. - data: Readable, - /// The image's format. Detected automatically by default. - #[named] - format: Option<Smart<ImageFormat>>, - /// The width of the image. - #[named] - width: Option<Smart<Rel<Length>>>, - /// The height of the image. - #[named] - height: Option<Smart<Rel<Length>>>, - /// A text describing the image. - #[named] - alt: Option<Option<EcoString>>, - /// How the image should adjust itself to a given area. - #[named] - fit: Option<ImageFit>, -) -> StrResult<Content> { - let mut elem = ImageElem::new(EcoString::new(), data); - if let Some(format) = format { - elem.push_format(format); - } - if let Some(width) = width { - elem.push_width(width); - } - if let Some(height) = height { - elem.push_height(height); - } - if let Some(alt) = alt { - elem.push_alt(alt); - } - if let Some(fit) = fit { - elem.push_fit(fit); +#[scope] +impl ImageElem { + /// Decode a raster or vector graphic from bytes or a string. + /// + /// ```example + /// #let original = read("diagram.svg") + /// #let changed = original.replace( + /// "#2B80FF", // blue + /// green.to-hex(), + /// ) + /// + /// #image.decode(original) + /// #image.decode(changed) + /// ``` + #[func(title = "Decode Image")] + pub fn decode( + /// The data to decode as an image. Can be a string for SVGs. + data: Readable, + /// The image's format. Detected automatically by default. + #[named] + format: Option<Smart<ImageFormat>>, + /// The width of the image. + #[named] + width: Option<Smart<Rel<Length>>>, + /// The height of the image. + #[named] + height: Option<Smart<Rel<Length>>>, + /// A text describing the image. + #[named] + alt: Option<Option<EcoString>>, + /// How the image should adjust itself to a given area. + #[named] + fit: Option<ImageFit>, + ) -> StrResult<Content> { + let mut elem = ImageElem::new(EcoString::new(), data); + if let Some(format) = format { + elem.push_format(format); + } + if let Some(width) = width { + elem.push_width(width); + } + if let Some(height) = height { + elem.push_height(height); + } + if let Some(alt) = alt { + elem.push_alt(alt); + } + if let Some(fit) = fit { + elem.push_fit(fit); + } + Ok(elem.pack()) } - Ok(elem.pack()) } impl Layout for ImageElem { @@ -175,8 +167,7 @@ impl Layout for ImageElem { let sizing = Axes::new(self.width(styles), self.height(styles)); let region = sizing - .zip(regions.base()) - .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))) + .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))) .unwrap_or(regions.base()); let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand; @@ -217,7 +208,7 @@ impl Layout for ImageElem { // process. let mut frame = Frame::new(fitted); frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span())); - frame.resize(target, Align::CENTER_HORIZON); + frame.resize(target, Axes::splat(FixedAlign::Center)); // Create a clipping group if only part of the image should be visible. if fit == ImageFit::Cover && !target.fits(fitted) { diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs index a476ffa7..9960a2d3 100644 --- a/crates/typst-library/src/visualize/line.rs +++ b/crates/typst-library/src/visualize/line.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// A line from one point to another. /// -/// ## Example { #example } +/// # Example /// ```example /// #set page(height: 100pt) /// @@ -13,10 +13,7 @@ use crate::prelude::*; /// stroke: 2pt + maroon, /// ) /// ``` -/// -/// Display: Line -/// Category: visualize -#[element(Layout)] +#[elem(Layout)] pub struct LineElem { /// The start point of the line. /// @@ -37,42 +34,7 @@ pub struct LineElem { /// respected if `end` is `none`. pub angle: Angle, - /// How to stroke the line. This can be: - /// - /// - A length specifying the stroke's thickness. The color is inherited, - /// defaulting to black. - /// - A color to use for the stroke. The thickness is inherited, defaulting - /// to `{1pt}`. - /// - A stroke combined from color and thickness using the `+` operator as - /// in `{2pt + red}`. - /// - A stroke described by a dictionary with any of the following keys: - /// - `paint`: The [color]($type/color) to use for the stroke. - /// - `thickness`: The stroke's thickness as a [length]($type/length). - /// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or - /// `{"square"}`. - /// - `join`: How sharp turns of a contour are rendered. One of - /// `{"miter"}`, `{"round"}`, or `{"bevel"}`. Not applicable to lines - /// but to [polygons]($func/polygon) or [paths]($func/path). - /// - `miter-limit`: Number at which protruding sharp angles are rendered - /// with a bevel instead. The higher the number, the sharper an angle - /// can be before it is bevelled. Only applicable if `join` is - /// `{"miter"}`. Defaults to `{4.0}`. - /// - `dash`: The dash pattern to use. Can be any of the following: - /// - One of the predefined patterns `{"solid"}`, `{"dotted"}`, - /// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`, - /// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`, - /// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}` - /// - An [array]($type/array) with alternating lengths for dashes and - /// gaps. You can also use the string `{"dot"}` for a length equal to - /// the line thickness. - /// - A [dictionary]($type/dictionary) with the keys `array` (same as - /// the array above), and `phase` (of type [length]($type/length)), - /// which defines where in the pattern to start drawing. - /// - /// On a `stroke` object, you can access any of the fields mentioned in the - /// dictionary format above. For example, `{(2pt + blue).thickness}` is - /// `{2pt}`, `{(2pt + blue).miter-limit}` is `{4.0}` (the default), and so - /// on. + /// How to [stroke]($stroke) the line. /// /// ```example /// #set line(length: 100%) @@ -86,7 +48,7 @@ pub struct LineElem { /// ``` #[resolve] #[fold] - pub stroke: PartialStroke, + pub stroke: Stroke, } impl Layout for LineElem { @@ -97,10 +59,8 @@ impl Layout for LineElem { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let resolve = |axes: Axes<Rel<Abs>>| { - axes.zip(regions.base()).map(|(l, b)| l.relative_to(b)) - }; - + let resolve = + |axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to); let start = resolve(self.start(styles)); let delta = self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index ea873f44..a013853f 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -16,30 +16,15 @@ use crate::prelude::*; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.define("image", ImageElem::func()); - global.define("line", LineElem::func()); - global.define("rect", RectElem::func()); - global.define("square", SquareElem::func()); - global.define("ellipse", EllipseElem::func()); - global.define("circle", CircleElem::func()); - global.define("polygon", PolygonElem::func()); - global.define("path", PathElem::func()); - global.define("black", Color::BLACK); - global.define("gray", Color::GRAY); - global.define("silver", Color::SILVER); - global.define("white", Color::WHITE); - global.define("navy", Color::NAVY); - global.define("blue", Color::BLUE); - global.define("aqua", Color::AQUA); - global.define("teal", Color::TEAL); - global.define("eastern", Color::EASTERN); - global.define("purple", Color::PURPLE); - global.define("fuchsia", Color::FUCHSIA); - global.define("maroon", Color::MAROON); - global.define("red", Color::RED); - global.define("orange", Color::ORANGE); - global.define("yellow", Color::YELLOW); - global.define("olive", Color::OLIVE); - global.define("green", Color::GREEN); - global.define("lime", Color::LIME); + global.category("visualize"); + global.define_type::<Color>(); + global.define_type::<Stroke>(); + global.define_elem::<ImageElem>(); + global.define_elem::<LineElem>(); + global.define_elem::<RectElem>(); + global.define_elem::<SquareElem>(); + global.define_elem::<EllipseElem>(); + global.define_elem::<CircleElem>(); + global.define_elem::<PolygonElem>(); + global.define_elem::<PathElem>(); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index d78abce1..c252e95f 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -7,7 +7,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; /// A path through a list of points, connected by Bezier curves. /// -/// ## Example { #example } +/// # Example /// ```example /// #path( /// fill: blue.lighten(80%), @@ -18,26 +18,24 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -/// -/// Display: Path -/// Category: visualize -#[element(Layout)] +#[elem(Layout)] pub struct PathElem { - /// How to fill the path. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// How to fill the path. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. /// - /// Currently all paths are filled according to the - /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). + /// Currently all paths are filled according to the [non-zero winding + /// rule](https://en.wikipedia.org/wiki/Nonzero-rule). pub fill: Option<Paint>, - /// How to stroke the path. This can be: + /// How to [stroke]($stroke) the path. This can be: /// - /// See the [line's documentation]($func/line.stroke) for more details. Can - /// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of - /// `{1pt}` black if and if only if no fill is given. + /// Can be set to `{none}` to disable the stroke or to `{auto}` for a + /// stroke of `{1pt}` black if and if only if no fill is given. #[resolve] #[fold] - pub stroke: Smart<Option<PartialStroke>>, + pub stroke: Smart<Option<Stroke>>, /// Whether to close this path with one last bezier curve. This curve will /// takes into account the adjacent control points. If you want to close @@ -50,8 +48,8 @@ pub struct PathElem { /// /// Each vertex can be defined in 3 ways: /// - /// - A regular point, as given to the [`line`]($func/line) or - /// [`polygon`]($func/polygon) function. + /// - A regular point, as given to the [`line`]($line) or + /// [`polygon`]($polygon) function. /// - An array of two points, the first being the vertex and the second /// being the control point. The control point is expressed relative to /// the vertex and is mirrored to get the second control point. The given @@ -60,7 +58,7 @@ pub struct PathElem { /// the curve going out of this vertex. /// - An array of three points, the first being the vertex and the next /// being the control points (control point for curves coming in and out, - /// respectively) + /// respectively). #[variadic] pub vertices: Vec<PathVertex>, } @@ -75,8 +73,7 @@ impl Layout for PathElem { ) -> SourceResult<Fragment> { let resolve = |axes: Axes<Rel<Length>>| { axes.resolve(styles) - .zip(regions.base()) - .map(|(l, b)| l.relative_to(b)) + .zip_map(regions.base(), Rel::relative_to) .to_point() }; @@ -136,9 +133,9 @@ impl Layout for PathElem { // Prepare fill and stroke. let fill = self.fill(styles); let stroke = match self.stroke(styles) { - Smart::Auto if fill.is_none() => Some(Stroke::default()), + Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default), + Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), }; let mut frame = Frame::new(size); diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index b244b2e9..9f573467 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -6,7 +6,7 @@ use crate::prelude::*; /// /// The polygon is defined by its corner points and is closed automatically. /// -/// ## Example { #example } +/// # Example /// ```example /// #polygon( /// fill: blue.lighten(80%), @@ -17,37 +17,102 @@ use crate::prelude::*; /// (0%, 2cm), /// ) /// ``` -/// -/// Display: Polygon -/// Category: visualize -#[element(Layout)] -#[scope( - scope.define("regular", polygon_regular_func()); - scope -)] +#[elem(scope, Layout)] pub struct PolygonElem { - /// How to fill the polygon. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// How to fill the polygon. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. /// /// Currently all polygons are filled according to the /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). pub fill: Option<Paint>, - /// How to stroke the polygon. This can be: + /// How to [stroke]($stroke) the polygon. This can be: /// - /// See the [line's documentation]($func/line.stroke) for more details. Can - /// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of - /// `{1pt}` black if and if only if no fill is given. + /// Can be set to `{none}` to disable the stroke or to `{auto}` for a + /// stroke of `{1pt}` black if and if only if no fill is given. #[resolve] #[fold] - pub stroke: Smart<Option<PartialStroke>>, + pub stroke: Smart<Option<Stroke>>, /// The vertices of the polygon. Each point is specified as an array of two - /// [relative lengths]($type/relative-length). + /// [relative lengths]($relative). #[variadic] pub vertices: Vec<Axes<Rel<Length>>>, } +#[scope] +impl PolygonElem { + /// A regular polygon, defined by its size and number of vertices. + /// + /// ```example + /// #polygon.regular( + /// fill: blue.lighten(80%), + /// stroke: blue, + /// size: 30pt, + /// vertices: 3, + /// ) + /// ``` + #[func(title = "Regular Polygon")] + pub fn regular( + /// How to fill the polygon. See the general + /// [polygon's documentation]($polygon.fill) for more details. + #[named] + fill: Option<Option<Paint>>, + + /// How to stroke the polygon. See the general + /// [polygon's documentation]($polygon.stroke) for more details. + #[named] + stroke: Option<Smart<Option<Stroke>>>, + + /// The diameter of the [circumcircle](https://en.wikipedia.org/wiki/Circumcircle) + /// of the regular polygon. + #[named] + #[default(Em::one().into())] + size: Length, + + /// The number of vertices in the polygon. + #[named] + #[default(3)] + vertices: u64, + ) -> Content { + let radius = size / 2.0; + let angle = |i: f64| { + 2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64) + }; + let (horizontal_offset, vertical_offset) = (0..=vertices) + .map(|v| { + ( + (radius * angle(v as f64).cos()) + radius, + (radius * angle(v as f64).sin()) + radius, + ) + }) + .fold((radius, radius), |(min_x, min_y), (v_x, v_y)| { + ( + if min_x < v_x { min_x } else { v_x }, + if min_y < v_y { min_y } else { v_y }, + ) + }); + let vertices = (0..=vertices) + .map(|v| { + let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset; + let y = (radius * angle(v as f64).sin()) + radius - vertical_offset; + Axes::new(x, y).map(Rel::from) + }) + .collect(); + + let mut elem = PolygonElem::new(vertices); + if let Some(fill) = fill { + elem.push_fill(fill); + } + if let Some(stroke) = stroke { + elem.push_stroke(stroke); + } + elem.pack() + } +} + impl Layout for PolygonElem { #[tracing::instrument(name = "PolygonElem::layout", skip_all)] fn layout( @@ -60,10 +125,7 @@ impl Layout for PolygonElem { .vertices() .iter() .map(|c| { - c.resolve(styles) - .zip(regions.base()) - .map(|(l, b)| l.relative_to(b)) - .to_point() + c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point() }) .collect(); @@ -78,9 +140,9 @@ impl Layout for PolygonElem { // Prepare fill and stroke. let fill = self.fill(styles); let stroke = match self.stroke(styles) { - Smart::Auto if fill.is_none() => Some(Stroke::default()), + Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default), + Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), }; // Construct a closed path given all points. @@ -97,71 +159,3 @@ impl Layout for PolygonElem { Ok(Fragment::frame(frame)) } } - -/// A regular polygon, defined by its size and number of vertices. -/// -/// ## Example { #example } -/// ```example -/// #polygon.regular( -/// fill: blue.lighten(80%), -/// stroke: blue, -/// size: 30pt, -/// vertices: 3, -/// ) -/// ``` -/// -/// Display: Regular Polygon -/// Category: visualize -#[func] -pub fn polygon_regular( - /// How to fill the polygon. See the general - /// [polygon's documentation]($func/polygon.fill) for more details. - #[named] - fill: Option<Option<Paint>>, - - /// How to stroke the polygon. See the general - /// [polygon's documentation]($func/polygon.stroke) for more details. - #[named] - stroke: Option<Smart<Option<PartialStroke>>>, - - /// The diameter of the circumcircle of the regular polygon (https://en.wikipedia.org/wiki/Circumcircle). - #[named] - #[default(Em::one().into())] - size: Length, - - /// The number of vertices in the polygon. - #[named] - #[default(3)] - vertices: u64, -) -> Content { - let radius = size / 2.0; - let angle = |i: f64| { - 2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64) - }; - let (horizontal_offset, vertical_offset) = (0..=vertices) - .map(|v| { - ( - (radius * angle(v as f64).cos()) + radius, - (radius * angle(v as f64).sin()) + radius, - ) - }) - .fold((radius, radius), |(min_x, min_y), (v_x, v_y)| { - (if min_x < v_x { min_x } else { v_x }, if min_y < v_y { min_y } else { v_y }) - }); - let vertices = (0..=vertices) - .map(|v| { - let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset; - let y = (radius * angle(v as f64).sin()) + radius - vertical_offset; - Axes::new(x, y).map(Rel::from) - }) - .collect(); - - let mut elem = PolygonElem::new(vertices); - if let Some(fill) = fill { - elem.push_fill(fill); - } - if let Some(stroke) = stroke { - elem.push_stroke(stroke); - } - elem.pack() -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 6129b70b..64d1ece6 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -4,7 +4,7 @@ use crate::prelude::*; /// A rectangle with optional content. /// -/// ## Example { #example } +/// # Example /// ```example /// // Without content. /// #rect(width: 35%, height: 30pt) @@ -15,10 +15,7 @@ use crate::prelude::*; /// to fit the content. /// ] /// ``` -/// -/// Display: Rectangle -/// Category: visualize -#[element(Layout)] +#[elem(title = "Rectangle", Layout)] pub struct RectElem { /// The rectangle's width, relative to its parent container. pub width: Smart<Rel<Length>>, @@ -41,8 +38,7 @@ pub struct RectElem { /// - `{none}` to disable stroking /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is /// given. - /// - Any kind of stroke that can also be used for - /// [lines]($func/line.stroke). + /// - Any kind of [stroke]($stroke) /// - A dictionary describing the stroke for each side inidvidually. The /// dictionary can contain the following keys in order of precedence: /// - `top`: The top stroke. @@ -65,7 +61,7 @@ pub struct RectElem { /// ``` #[resolve] #[fold] - pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>, + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, /// How much to round the rectangle's corners, relative to the minimum of /// the width and height divided by two. This can be: @@ -106,20 +102,14 @@ pub struct RectElem { pub radius: Corners<Option<Rel<Length>>>, /// How much to pad the rectangle's content. - /// - /// _Note:_ When the rectangle contains text, its exact size depends on the - /// current [text edges]($func/text.top-edge). - /// - /// ```example - /// #rect(inset: 0pt)[Tight] - /// ``` + /// See the [box's documentation]($box.outset) for more details. #[resolve] #[fold] #[default(Sides::splat(Abs::pt(5.0).into()))] pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the rectangle's size without affecting the layout. - /// See the [box's documentation]($func/box.outset) for more details. + /// See the [box's documentation]($box.outset) for more details. #[resolve] #[fold] pub outset: Sides<Option<Rel<Length>>>, @@ -159,7 +149,7 @@ impl Layout for RectElem { /// A square with optional content. /// -/// ## Example { #example } +/// # Example /// ```example /// // Without content. /// #square(size: 40pt) @@ -170,10 +160,7 @@ impl Layout for RectElem { /// sized to fit. /// ] /// ``` -/// -/// Display: Square -/// Category: visualize -#[element(Layout)] +#[elem(Layout)] pub struct SquareElem { /// The square's side length. This is mutually exclusive with `width` and /// `height`. @@ -203,31 +190,31 @@ pub struct SquareElem { })] pub height: Smart<Rel<Length>>, - /// How to fill the square. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// How to fill the square. See the [rectangle's documentation]($rect.fill) + /// for more details. pub fill: Option<Paint>, - /// How to stroke the square. See the [rectangle's - /// documentation]($func/rect.stroke) for more details. + /// How to stroke the square. See the + /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] - pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>, + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, - /// How much to round the square's corners. See the [rectangle's - /// documentation]($func/rect.radius) for more details. + /// How much to round the square's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. #[resolve] #[fold] pub radius: Corners<Option<Rel<Length>>>, - /// How much to pad the square's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. + /// How much to pad the square's content. See the + /// [box's documentation]($box.inset) for more details. #[resolve] #[fold] #[default(Sides::splat(Abs::pt(5.0).into()))] pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the square's size without affecting the layout. See - /// the [rectangle's documentation]($func/rect.outset) for more details. + /// the [box's documentation]($box.outset) for more details. #[resolve] #[fold] pub outset: Sides<Option<Rel<Length>>>, @@ -268,7 +255,7 @@ impl Layout for SquareElem { /// An ellipse with optional content. /// -/// ## Example { #example } +/// # Example /// ```example /// // Without content. /// #ellipse(width: 35%, height: 30pt) @@ -280,10 +267,7 @@ impl Layout for SquareElem { /// to fit the content. /// ] /// ``` -/// -/// Display: Ellipse -/// Category: visualize -#[element(Layout)] +#[elem(Layout)] pub struct EllipseElem { /// The ellipse's width, relative to its parent container. pub width: Smart<Rel<Length>>, @@ -291,25 +275,25 @@ pub struct EllipseElem { /// The ellipse's height, relative to its parent container. pub height: Smart<Rel<Length>>, - /// How to fill the ellipse. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) + /// for more details. pub fill: Option<Paint>, - /// How to stroke the ellipse. See the [rectangle's - /// documentation]($func/rect.stroke) for more details. + /// How to stroke the ellipse. See the + /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] - pub stroke: Smart<Option<PartialStroke>>, + pub stroke: Smart<Option<Stroke>>, - /// How much to pad the ellipse's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. + /// How much to pad the ellipse's content. See the + /// [box's documentation]($box.inset) for more details. #[resolve] #[fold] #[default(Sides::splat(Abs::pt(5.0).into()))] pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the ellipse's size without affecting the layout. See - /// the [rectangle's documentation]($func/rect.outset) for more details. + /// the [box's documentation]($box.outset) for more details. #[resolve] #[fold] pub outset: Sides<Option<Rel<Length>>>, @@ -349,7 +333,7 @@ impl Layout for EllipseElem { /// A circle with optional content. /// -/// ## Example { #example } +/// # Example /// ```example /// // Without content. /// #circle(radius: 25pt) @@ -361,10 +345,7 @@ impl Layout for EllipseElem { /// sized to fit. /// ] /// ``` -/// -/// Display: Circle -/// Category: visualize -#[element(Layout)] +#[elem(Layout)] pub struct CircleElem { /// The circle's radius. This is mutually exclusive with `width` and /// `height`. @@ -398,26 +379,26 @@ pub struct CircleElem { })] pub height: Smart<Rel<Length>>, - /// How to fill the circle. See the - /// [rectangle's documentation]($func/rect.fill) for more details. + /// How to fill the circle. See the [rectangle's documentation]($rect.fill) + /// for more details. pub fill: Option<Paint>, - /// How to stroke the circle. See the [rectangle's - /// documentation]($func/rect.stroke) for more details. + /// How to stroke the circle. See the + /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] #[default(Smart::Auto)] - pub stroke: Smart<Option<PartialStroke>>, + pub stroke: Smart<Option<Stroke>>, - /// How much to pad the circle's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. + /// How much to pad the circle's content. See the + /// [box's documentation]($box.inset) for more details. #[resolve] #[fold] #[default(Sides::splat(Abs::pt(5.0).into()))] pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the circle's size without affecting the layout. See - /// the [rectangle's documentation]($func/rect.outset) for more details. + /// the [box's documentation]($box.outset) for more details. #[resolve] #[fold] pub outset: Sides<Option<Rel<Length>>>, @@ -464,15 +445,14 @@ fn layout( body: &Option<Content>, sizing: Axes<Smart<Rel<Length>>>, fill: Option<Paint>, - stroke: Smart<Sides<Option<PartialStroke<Abs>>>>, + stroke: Smart<Sides<Option<Stroke<Abs>>>>, mut inset: Sides<Rel<Abs>>, outset: Sides<Rel<Abs>>, radius: Corners<Rel<Abs>>, span: Span, ) -> SourceResult<Fragment> { let resolved = sizing - .zip(regions.base()) - .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))); + .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))); let mut frame; if let Some(child) = body { @@ -517,11 +497,9 @@ fn layout( // Prepare stroke. let stroke = match stroke { - Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), + Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())), Smart::Auto => Sides::splat(None), - Smart::Custom(strokes) => { - strokes.map(|s| s.map(PartialStroke::unwrap_or_default)) - } + Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)), }; // Add fill and/or stroke. diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index ec25a336..fe56e5dd 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -17,7 +17,7 @@ bench = false [dependencies] comemo = "0.3" -ecow = { version = "0.1.1", features = ["serde"] } +ecow = { version = "0.1.2", features = ["serde"] } once_cell = "1" serde = { version = "1.0.184", features = ["derive"] } tracing = "0.1.37" diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index 2634d6d0..e3c4c6c1 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -22,7 +22,7 @@ base64 = "0.21.2" bitflags = { version = "2", features = ["serde"] } bytemuck = "1" comemo = "0.3" -ecow = { version = "0.1.1", features = ["serde"] } +ecow = { version = "0.1.2", features = ["serde"] } flate2 = "1" fontdb = "0.13" if_chain = "1" diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs index a6379d65..846ab78f 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -41,7 +41,10 @@ macro_rules! __bail { } #[doc(inline)] -pub use crate::__bail as bail; +pub use crate::{__bail as bail, __error as error, __warning as warning}; + +#[doc(hidden)] +pub use ecow::{eco_format, EcoString}; /// Construct an [`EcoString`] or [`SourceDiagnostic`] with severity `Error`. #[macro_export] @@ -71,13 +74,6 @@ macro_rules! __warning { }; } -#[doc(inline)] -pub use crate::__error as error; -#[doc(inline)] -pub use crate::__warning as warning; -#[doc(hidden)] -pub use ecow::{eco_format, EcoString}; - /// A result that can carry multiple source errors. pub type SourceResult<T> = Result<T, Box<Vec<SourceDiagnostic>>>; diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs index 6f005097..20e03b6b 100644 --- a/crates/typst/src/doc.rs +++ b/crates/typst/src/doc.rs @@ -8,11 +8,12 @@ use std::sync::Arc; use ecow::EcoString; -use crate::eval::{cast, dict, Dict, Value}; +use crate::eval::{cast, dict, ty, Dict, Value}; use crate::font::Font; use crate::geom::{ - self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, - Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, + self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, + Geometry, Length, Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, + Transform, }; use crate::image::Image; use crate::model::{Content, Location, MetaElem, StyleChain}; @@ -231,14 +232,11 @@ impl Frame { /// Resize the frame to a new size, distributing new space according to the /// given alignments. - pub fn resize(&mut self, target: Size, aligns: Axes<Align>) { + pub fn resize(&mut self, target: Size, align: Axes<FixedAlign>) { if self.size != target { - let offset = Point::new( - aligns.x.position(target.x - self.size.x), - aligns.y.position(target.y - self.size.y), - ); + let offset = align.zip_map(target - self.size, FixedAlign::position); self.size = target; - self.translate(offset); + self.translate(offset.to_point()); } } @@ -290,7 +288,7 @@ impl Frame { pub fn fill_and_stroke( &mut self, fill: Option<Paint>, - stroke: Sides<Option<Stroke>>, + stroke: Sides<Option<FixedStroke>>, outset: Sides<Rel<Abs>>, radius: Corners<Rel<Abs>>, span: Span, @@ -357,10 +355,10 @@ impl Frame { 1, Point::with_y(self.baseline()), FrameItem::Shape( - Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { + Geometry::Line(Point::with_x(self.size.x)).stroked(FixedStroke { paint: Color::RED.into(), thickness: Abs::pt(1.0), - ..Stroke::default() + ..FixedStroke::default() }), Span::detached(), ), @@ -384,10 +382,10 @@ impl Frame { self.push( Point::with_y(y), FrameItem::Shape( - Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { + Geometry::Line(Point::with_x(self.size.x)).stroked(FixedStroke { paint: Color::GREEN.into(), thickness: Abs::pt(1.0), - ..Stroke::default() + ..FixedStroke::default() }), Span::detached(), ), @@ -660,6 +658,7 @@ cast! { } /// Meta information that isn't visible or renderable. +#[ty] #[derive(Clone, PartialEq, Hash)] pub enum Meta { /// An internal or external link to a destination. @@ -676,7 +675,7 @@ pub enum Meta { } cast! { - type Meta: "meta", + type Meta, } impl Debug for Meta { diff --git a/crates/typst/src/eval/args.rs b/crates/typst/src/eval/args.rs index 81dd5845..ac403eef 100644 --- a/crates/typst/src/eval/args.rs +++ b/crates/typst/src/eval/args.rs @@ -2,12 +2,42 @@ use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, EcoVec}; -use super::{Array, Dict, FromValue, IntoValue, Str, Value}; -use crate::diag::{bail, At, SourceResult}; +use super::{func, scope, ty, Array, Dict, FromValue, IntoValue, Str, Value}; +use crate::diag::{bail, At, SourceDiagnostic, SourceResult}; use crate::syntax::{Span, Spanned}; use crate::util::pretty_array_like; -/// Evaluated arguments to a function. +/// Captured arguments to a function. +/// +/// # Argument Sinks +/// Like built-in functions, custom functions can also take a variable number of +/// arguments. You can specify an _argument sink_ which collects all excess +/// arguments as `..sink`. The resulting `sink` value is of the `arguments` +/// type. It exposes methods to access the positional and named arguments. +/// +/// ```example +/// #let format(title, ..authors) = { +/// let by = authors +/// .pos() +/// .join(", ", last: " and ") +/// +/// [*#title* \ _Written by #by;_] +/// } +/// +/// #format("ArtosFlow", "Jane", "Joe") +/// ``` +/// +/// # Spreading +/// Inversely to an argument sink, you can _spread_ arguments, arrays and +/// dictionaries into a function call with the `..spread` operator: +/// +/// ```example +/// #let array = (2, 3, 5) +/// #calc.min(..array) +/// #let dict = (fill: blue) +/// #text(..dict)[Hello] +/// ``` +#[ty(scope, name = "arguments")] #[derive(Clone, PartialEq, Hash)] pub struct Args { /// The span of the whole argument list. @@ -125,8 +155,21 @@ impl Args { T: FromValue<Spanned<Value>>, { let mut list = vec![]; - while let Some(value) = self.find()? { - list.push(value); + let mut errors = vec![]; + self.items.retain(|item| { + if item.name.is_some() { + return true; + }; + let span = item.value.span; + let spanned = Spanned::new(std::mem::take(&mut item.value.v), span); + match T::from_value(spanned) { + Ok(val) => list.push(val), + Err(err) => errors.push(SourceDiagnostic::error(span, err)), + } + false + }); + if !errors.is_empty() { + return Err(Box::new(errors)); } Ok(list) } @@ -183,8 +226,12 @@ impl Args { } Ok(()) } +} - /// Extract the positional arguments as an array. +#[scope] +impl Args { + /// Returns the captured positional arguments as an array. + #[func(name = "pos", title = "Positional")] pub fn to_pos(&self) -> Array { self.items .iter() @@ -193,7 +240,8 @@ impl Args { .collect() } - /// Extract the named arguments as a dictionary. + /// Returns the captured named arguments as a dictionary. + #[func(name = "named")] pub fn to_named(&self) -> Dict { self.items .iter() diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs index 41def66c..34bfc80e 100644 --- a/crates/typst/src/eval/array.rs +++ b/crates/typst/src/eval/array.rs @@ -1,11 +1,15 @@ use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroI64; use std::ops::{Add, AddAssign}; use ecow::{eco_format, EcoString, EcoVec}; use serde::{Deserialize, Serialize}; -use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; +use super::{ + cast, func, ops, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, + Reflect, Value, Vm, +}; use crate::diag::{At, SourceResult, StrResult}; use crate::eval::ops::{add, mul}; use crate::syntax::Span; @@ -31,10 +35,41 @@ macro_rules! __array { #[doc(inline)] pub use crate::__array as array; + #[doc(hidden)] pub use ecow::eco_vec; -/// A reference counted array with value semantics. +/// A sequence of values. +/// +/// You can construct an array by enclosing a comma-separated sequence of values +/// in parentheses. The values do not have to be of the same type. +/// +/// You can access and update array items with the `.at()` method. Indices are +/// zero-based and negative indices wrap around to the end of the array. You can +/// iterate over an array using a [for loop]($scripting/#loops). Arrays can be +/// added together with the `+` operator, [joined together]($scripting/#blocks) +/// and multiplied with integers. +/// +/// **Note:** An array of length one needs a trailing comma, as in `{(1,)}`. +/// This is to disambiguate from a simple parenthesized expressions like `{(1 + +/// 2) * 3}`. An empty array is written as `{()}`. +/// +/// # Example +/// ```example +/// #let values = (1, 7, 4, -3, 2) +/// +/// #values.at(0) \ +/// #(values.at(0) = 3) +/// #values.at(-1) \ +/// #values.find(calc.even) \ +/// #values.filter(calc.odd) \ +/// #values.map(calc.abs) \ +/// #values.rev() \ +/// #(1, (2, 3)).flatten() \ +/// #(("A", "B", "C") +/// .join(", ", last: " and ")) +/// ``` +#[ty(scope)] #[derive(Default, Clone, PartialEq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Array(EcoVec<Value>); @@ -52,17 +87,17 @@ impl Array { /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.len() == 0 + self.0.is_empty() } - /// The length of the array. - pub fn len(&self) -> usize { - self.0.len() + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() } - /// The first value in the array. - pub fn first(&self) -> StrResult<&Value> { - self.0.first().ok_or_else(array_is_empty) + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter<Value> { + self.0.iter() } /// Mutably borrow the first value in the array. @@ -70,24 +105,11 @@ impl Array { self.0.make_mut().first_mut().ok_or_else(array_is_empty) } - /// The last value in the array. - pub fn last(&self) -> StrResult<&Value> { - self.0.last().ok_or_else(array_is_empty) - } - /// Mutably borrow the last value in the array. pub fn last_mut(&mut self) -> StrResult<&mut Value> { self.0.make_mut().last_mut().ok_or_else(array_is_empty) } - /// Borrow the value at the given index. - pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> { - self.locate_opt(index, false) - .and_then(|i| self.0.get(i).cloned()) - .or(default) - .ok_or_else(|| out_of_bounds_no_default(index, self.len())) - } - /// Mutably borrow the value at the given index. pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { let len = self.len(); @@ -96,57 +118,210 @@ impl Array { .ok_or_else(|| out_of_bounds_no_default(index, len)) } - /// Push a value to the end of the array. - pub fn push(&mut self, value: Value) { + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> { + self.locate_opt(index, end_ok) + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds if and only if `end_ok` is true. + fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v < self.0.len() + end_ok as usize) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + let count = self + .len() + .checked_mul(n) + .ok_or_else(|| format!("cannot repeat this array {} times", n))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } +} + +#[scope] +impl Array { + /// Converts a value to an array. + /// + /// Note that this function is only intended for conversion of a collection-like + /// value to an array, not for creation of an array from individual items. Use + /// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead. + /// + /// ```example + /// #let hi = "Hello 😃" + /// #array(bytes(hi)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an array. + value: ToArray, + ) -> Array { + value.0 + } + + /// The number of values in the array. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the first item in the array. May be used on the left-hand side + /// of an assignment. Fails with an error if the array is empty. + #[func] + pub fn first(&self) -> StrResult<Value> { + self.0.first().cloned().ok_or_else(array_is_empty) + } + + /// Returns the last item in the array. May be used on the left-hand side of + /// an assignment. Fails with an error if the array is empty. + #[func] + pub fn last(&self) -> StrResult<Value> { + self.0.last().cloned().ok_or_else(array_is_empty) + } + + /// Returns the item at the specified index in the array. May be used on the + /// left-hand side of an assignment. Returns the default value if the index + /// is out of bounds or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index, false) + .and_then(|i| self.0.get(i).cloned()) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Add a value to the end of the array. + #[func] + pub fn push( + &mut self, + /// The value to insert at the end of the array. + value: Value, + ) { self.0.push(value); } - /// Remove the last value in the array. + /// Remove the last item from the array and return it. Fails with an error + /// if the array is empty. + #[func] pub fn pop(&mut self) -> StrResult<Value> { self.0.pop().ok_or_else(array_is_empty) } - /// Insert a value at the specified index. - pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> { + /// Insert a value into the array at the specified index. Fails with an + /// error if the index is out of bounds. + #[func] + pub fn insert( + &mut self, + /// The index at which to insert the item. If negative, indexes from + /// the back. + index: i64, + /// The value to insert into the array. + value: Value, + ) -> StrResult<()> { let i = self.locate(index, true)?; self.0.insert(i, value); Ok(()) } - /// Remove and return the value at the specified index. - pub fn remove(&mut self, index: i64) -> StrResult<Value> { + /// Remove the value at the specified index from the array and return it. + #[func] + pub fn remove( + &mut self, + /// The index at which to remove the item. If negative, indexes from + /// the back. + index: i64, + ) -> StrResult<Value> { let i = self.locate(index, false)?; Ok(self.0.remove(i)) } - /// Extract a contiguous subregion of the array. - pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + /// Extract a subslice of the array. Fails with an error if the start or + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). If negative, indexes from the back. + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// of the array is extracted. If negative, indexes from the back. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Array> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } let start = self.locate(start, true)?; let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start); Ok(self.0[start..end].into()) } - /// Whether the array contains a specific value. - pub fn contains(&self, value: &Value) -> bool { - self.0.contains(value) + /// Whether the array contains the specified value. + /// + /// This method also has dedicated syntax: You can write `{2 in (1, 2, 3)}` + /// instead of `{(1, 2, 3).contains(2)}`. + #[func] + pub fn contains( + &self, + /// The value to search for. + value: Value, + ) -> bool { + self.0.contains(&value) } - /// Return the first matching item. - pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<Value>> { + /// Searches for an item for which the given function returns `{true}` and + /// returns the first match or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<Value>> { for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + let args = Args::new(searcher.span(), [item.clone()]); + if searcher.call_vm(vm, args)?.cast::<bool>().at(searcher.span())? { return Ok(Some(item.clone())); } } Ok(None) } - /// Return the index of the first matching item. - pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<i64>> { + /// Searches for an item for which the given function returns `{true}` and + /// returns the index of the first match or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<i64>> { for (i, item) in self.iter().enumerate() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + let args = Args::new(searcher.span(), [item.clone()]); + if searcher.call_vm(vm, args)?.cast::<bool>().at(searcher.span())? { return Ok(Some(i as i64)); } } @@ -154,78 +329,259 @@ impl Array { Ok(None) } - /// Return a new array with only those items for which the function returns - /// true. - pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + /// Create an array consisting of a sequence of numbers. + /// + /// If you pass just one positional parameter, it is interpreted as the + /// `end` of the range. If you pass two, they describe the `start` and `end` + /// of the range. + /// + /// This function is available both in the array function's scope and + /// globally. + /// + /// ```example + /// #range(5) \ + /// #range(2, 5) \ + /// #range(20, step: 4) \ + /// #range(21, step: 4) \ + /// #range(5, 2, step: -1) + /// ``` + #[func] + pub fn range( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: Args, + /// The start of the range (inclusive). + #[external] + #[default] + start: i64, + /// The end of the range (exclusive). + #[external] + end: i64, + /// The distance between the generated numbers. + #[named] + #[default(NonZeroI64::new(1).unwrap())] + step: NonZeroI64, + ) -> SourceResult<Array> { + let mut args = args; + let first = args.expect::<i64>("end")?; + let (start, end) = match args.eat::<i64>()? { + Some(second) => (first, second), + None => (0, first), + }; + args.finish()?; + + let step = step.get(); + + let mut x = start; + let mut array = Self::new(); + + while x.cmp(&end) == 0.cmp(&step) { + array.push(x.into_value()); + x += step; + } + + Ok(array) + } + + /// Produces a new array with only the items from the original one for which + /// the given function returns true. + #[func] + pub fn filter( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<Array> { let mut kept = EcoVec::new(); for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + let args = Args::new(test.span(), [item.clone()]); + if test.call_vm(vm, args)?.cast::<bool>().at(test.span())? { kept.push(item.clone()) } } Ok(kept.into()) } - /// Transform each item in the array with a function. - pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + /// Produces a new array in which all items from the original one were + /// transformed with the given function. + #[func] + pub fn map( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. + mapper: Func, + ) -> SourceResult<Array> { self.iter() .map(|item| { - let args = Args::new(func.span(), [item.clone()]); - func.call_vm(vm, args) + let args = Args::new(mapper.span(), [item.clone()]); + mapper.call_vm(vm, args) }) .collect() } - /// Fold all of the array's items into one with a function. - pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult<Value> { + /// Returns a new array with the values alongside their indices. + /// + /// The returned array consists of `(index, value)` pairs in the form of + /// length-2 arrays. These can be [destructured]($scripting/#bindings) with + /// a let binding or for loop. + #[func] + pub fn enumerate( + &self, + /// The index returned for the first pair of the returned list. + #[named] + #[default(0)] + start: i64, + ) -> StrResult<Array> { + self.iter() + .enumerate() + .map(|(i, value)| { + Ok(array![ + start + .checked_add_unsigned(i as u64) + .ok_or("array index is too large")?, + value.clone() + ] + .into_value()) + }) + .collect() + } + + /// Zips the array with other arrays. If the arrays are of unequal length, + /// it will only zip up until the last element of the shortest array and the + /// remaining elements will be ignored. The return value is an array where + /// each element is yet another array, the size of each of those is the + /// number of zipped arrays. + /// + /// This function is variadic, meaning that you can zip multiple arrays + /// together at once: `{(1, 2, 3).zip((3, 4, 5), (6, 7, 8))}` yields + /// `{((1, 3, 6), (2, 4, 7), (3, 5, 8))}`. + #[func] + pub fn zip( + &self, + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: Args, + /// The arrays to zip with. + #[external] + #[variadic] + others: Vec<Array>, + ) -> SourceResult<Array> { + // Fast path for just two arrays. + let mut args = args; + if args.remaining() <= 1 { + let other = args.expect::<Array>("others")?; + args.finish()?; + return Ok(self + .iter() + .zip(other) + .map(|(first, second)| array![first.clone(), second].into_value()) + .collect()); + } + + // If there is more than one array, we use the manual method. + let mut out = Self::with_capacity(self.len()); + let mut iterators = args + .all::<Array>()? + .into_iter() + .map(|i| i.into_iter()) + .collect::<Vec<_>>(); + args.finish()?; + + for this in self.iter() { + let mut row = Self::with_capacity(1 + iterators.len()); + row.push(this.clone()); + + for iterator in &mut iterators { + let Some(item) = iterator.next() else { + return Ok(out); + }; + + row.push(item); + } + + out.push(row.into_value()); + } + + Ok(out) + } + + /// Folds all items into a single value using an accumulator function. + #[func] + pub fn fold( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The initial value to start with. + init: Value, + /// The folding function. Must have two parameters: One for the + /// accumulated value and one for an item. + folder: Func, + ) -> SourceResult<Value> { let mut acc = init; for item in self.iter() { - let args = Args::new(func.span(), [acc, item.clone()]); - acc = func.call_vm(vm, args)?; + let args = Args::new(folder.span(), [acc, item.clone()]); + acc = folder.call_vm(vm, args)?; } Ok(acc) } - /// Calculates the sum of the array's items - pub fn sum(&self, default: Option<Value>, span: Span) -> SourceResult<Value> { + /// Sums all items (works for all types that can be added). + #[func] + pub fn sum( + &self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { let mut acc = self + .0 .first() - .map(|x| x.clone()) - .or_else(|_| { - default.ok_or_else(|| { - eco_format!("cannot calculate sum of empty array with no default") - }) - }) - .at(span)?; + .cloned() + .or(default) + .ok_or("cannot calculate sum of empty array with no default")?; for i in self.iter().skip(1) { - acc = add(acc, i.clone()).at(span)?; + acc = add(acc, i.clone())?; } Ok(acc) } - /// Calculates the product of the array's items - pub fn product(&self, default: Option<Value>, span: Span) -> SourceResult<Value> { + /// Calculates the product all items (works for all types that can be + /// multiplied). + #[func] + pub fn product( + &self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { let mut acc = self + .0 .first() - .map(|x| x.clone()) - .or_else(|_| { - default.ok_or_else(|| { - eco_format!("cannot calculate product of empty array with no default") - }) - }) - .at(span)?; + .cloned() + .or(default) + .ok_or("cannot calculate product of empty array with no default")?; for i in self.iter().skip(1) { - acc = mul(acc, i.clone()).at(span)?; + acc = mul(acc, i.clone())?; } Ok(acc) } - /// Whether any item matches. - pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + /// Whether the given function returns `{true}` for any item in the array. + #[func] + pub fn any( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + let args = Args::new(test.span(), [item.clone()]); + if test.call_vm(vm, args)?.cast::<bool>().at(test.span())? { return Ok(true); } } @@ -233,11 +589,18 @@ impl Array { Ok(false) } - /// Whether all items match. - pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + /// Whether the given function returns `{true}` for all items in the array. + #[func] + pub fn all( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if !func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + let args = Args::new(test.span(), [item.clone()]); + if !test.call_vm(vm, args)?.cast::<bool>().at(test.span())? { return Ok(false); } } @@ -245,8 +608,9 @@ impl Array { Ok(true) } - /// Return a new array with all items from this and nested arrays. - pub fn flatten(&self) -> Self { + /// Combine all nested arrays into a single flat one. + #[func] + pub fn flatten(&self) -> Array { let mut flat = EcoVec::with_capacity(self.0.len()); for item in self.iter() { if let Value::Array(nested) = item { @@ -258,32 +622,47 @@ impl Array { flat.into() } - /// Returns a new array with reversed order. - pub fn rev(&self) -> Self { + /// Return a new array with the same items, but in reverse order. + #[func(title = "Reverse")] + pub fn rev(&self) -> Array { self.0.iter().cloned().rev().collect() } - /// Split all values in the array. - pub fn split(&self, at: Value) -> Array { + /// Split the array at occurrences of the specified value. + #[func] + pub fn split( + &self, + /// The value to split at. + at: Value, + ) -> Array { self.as_slice() .split(|value| *value == at) .map(|subslice| Value::Array(subslice.iter().cloned().collect())) .collect() } - /// Join all values in the array, optionally with separator and last - /// separator (between the final two items). - pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> { + /// Combine all items in the array into one. + #[func] + pub fn join( + &self, + /// A value to insert between each item of the array. + #[default] + separator: Option<Value>, + /// An alternative separator between the last two items + #[named] + last: Option<Value>, + ) -> StrResult<Value> { let len = self.0.len(); - let sep = sep.unwrap_or(Value::None); + let separator = separator.unwrap_or(Value::None); + let mut last = last; let mut result = Value::None; for (i, value) in self.iter().cloned().enumerate() { if i > 0 { if i + 1 == len && last.is_some() { result = ops::join(result, last.take().unwrap())?; } else { - result = ops::join(result, sep.clone())?; + result = ops::join(result, separator.clone())?; } } @@ -295,9 +674,10 @@ impl Array { /// Returns an array with a copy of the separator value placed between /// adjacent elements. + #[func] pub fn intersperse(&self, sep: Value) -> Array { - // TODO: Use https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse - // once it is stabilized. + // TODO: Use once stabilized: + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse let size = match self.len() { 0 => return Array::new(), n => (2 * n) - 1, @@ -317,57 +697,23 @@ impl Array { Array(vec) } - /// The method `array.zip`, depending on the arguments, it automatically - /// detects whether it should use the single zip operator, which depends - /// on the standard library's implementation and can therefore be faster. - /// Or it zips using a manual implementation which allows for zipping more - /// than two arrays at once. - pub fn zip(&self, args: &mut Args) -> SourceResult<Self> { - // Fast path for just two arrays. - if args.remaining() <= 1 { - return Ok(self - .iter() - .zip(args.expect::<Array>("others")?) - .map(|(first, second)| array![first.clone(), second].into_value()) - .collect()); - } - - // If there is more than one array, we use the manual method. - let mut out = Self::with_capacity(self.len()); - let mut iterators = args - .all::<Array>()? - .into_iter() - .map(|i| i.into_iter()) - .collect::<Vec<_>>(); - - for this in self.iter() { - let mut row = Self::with_capacity(1 + iterators.len()); - row.push(this.clone()); - - for iterator in &mut iterators { - let Some(item) = iterator.next() else { - return Ok(out); - }; - - row.push(item); - } - - out.push(row.into_value()); - } - - Ok(out) - } - - /// Return a sorted version of this array, optionally by a given key function. + /// Return a sorted version of this array, optionally by a given key + /// function. /// - /// Returns an error if two values could not be compared or if the key function (if given) - /// yields an error. + /// Returns an error if two values could not be compared or if the key + /// function (if given) yields an error. + #[func] pub fn sorted( &self, + /// The virtual machine. vm: &mut Vm, + /// The callsite span. span: Span, + /// If given, applies this function to the elements in the array to + /// determine the keys to sort by. + #[named] key: Option<Func>, - ) -> SourceResult<Self> { + ) -> SourceResult<Array> { let mut result = Ok(()); let mut vec = self.0.clone(); let mut key_of = |x: Value| match &key { @@ -398,34 +744,24 @@ impl Array { result.map(|_| vec.into()) } - /// Repeat this array `n` times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let count = usize::try_from(n) - .ok() - .and_then(|n| self.0.len().checked_mul(n)) - .ok_or_else(|| format!("cannot repeat this array {} times", n))?; - - Ok(self.iter().cloned().cycle().take(count).collect()) - } - - /// Enumerate all items in the array. - pub fn enumerate(&self, start: i64) -> StrResult<Self> { - self.iter() - .enumerate() - .map(|(i, value)| { - Ok(array![ - start - .checked_add_unsigned(i as u64) - .ok_or_else(|| "array index is too large".to_string())?, - value.clone() - ] - .into_value()) - }) - .collect() - } - /// Deduplicates all items in the array. - pub fn dedup(&self, vm: &mut Vm, key: Option<Func>) -> SourceResult<Self> { + /// + /// Returns a new array with all duplicate items removed. Only the first + /// element of each duplicate is kept. + /// + /// ```example + /// #(1, 1, 2, 3, 1).dedup() + /// ``` + #[func(title = "Deduplicate")] + pub fn dedup( + &self, + /// The virtual machine. + vm: &mut Vm, + /// If given, applies this function to the elements in the array to + /// determine the keys to deduplicate by. + #[named] + key: Option<Func>, + ) -> SourceResult<Array> { let mut out = EcoVec::with_capacity(self.0.len()); let mut key_of = |x: Value| match &key { // NOTE: We are relying on `comemo`'s memoization of function @@ -455,34 +791,15 @@ impl Array { Ok(Self(out)) } +} - /// Extract a slice of the whole array. - pub fn as_slice(&self) -> &[Value] { - self.0.as_slice() - } - - /// Iterate over references to the contained values. - pub fn iter(&self) -> std::slice::Iter<Value> { - self.0.iter() - } - - /// Resolve an index or throw an out of bounds error. - fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> { - self.locate_opt(index, end_ok) - .ok_or_else(|| out_of_bounds(index, self.len())) - } - - /// Resolve an index, if it is within bounds. - /// - /// `index == len` is considered in bounds if and only if `end_ok` is true. - fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> { - let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; +/// A value that can be cast to bytes. +pub struct ToArray(Array); - wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v < self.0.len() + end_ok as usize) - } +cast! { + ToArray, + v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()), + v: Array => Self(v), } impl Debug for Array { @@ -555,8 +872,12 @@ impl From<&[Value]> for Array { } impl<T> Reflect for Vec<T> { - fn describe() -> CastInfo { - Array::describe() + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() } fn castable(value: &Value) -> bool { diff --git a/crates/typst/src/eval/auto.rs b/crates/typst/src/eval/auto.rs index e73b3f33..a9d8fc9e 100644 --- a/crates/typst/src/eval/auto.rs +++ b/crates/typst/src/eval/auto.rs @@ -1,9 +1,17 @@ use std::fmt::{self, Debug, Formatter}; -use super::{CastInfo, FromValue, IntoValue, Reflect, Value}; +use super::{ty, CastInfo, FromValue, IntoValue, Reflect, Type, Value}; use crate::diag::StrResult; /// A value that indicates a smart default. +/// +/// The auto type has exactly one value: `{auto}`. +/// +/// Parameters that support the `{auto}` value have some smart default or +/// contextual behaviour. A good example is the [text direction]($text.dir) +/// parameter. Setting it to `{auto}` lets Typst automatically determine the +/// direction from the [text language]($text.lang). +#[ty(name = "auto")] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct AutoValue; @@ -23,8 +31,12 @@ impl FromValue for AutoValue { } impl Reflect for AutoValue { - fn describe() -> CastInfo { - CastInfo::Type("auto") + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) } fn castable(value: &Value) -> bool { diff --git a/crates/typst/src/eval/bool.rs b/crates/typst/src/eval/bool.rs new file mode 100644 index 00000000..dcc73e66 --- /dev/null +++ b/crates/typst/src/eval/bool.rs @@ -0,0 +1,15 @@ +use super::ty; + +/// A type with two states. +/// +/// The boolean type has two values: `{true}` and `{false}`. It denotes whether +/// something is active or enabled. +/// +/// # Example +/// ```example +/// #false \ +/// #true \ +/// #(1 < 2) +/// ``` +#[ty(title = "Boolean")] +type bool; diff --git a/crates/typst/src/eval/bytes.rs b/crates/typst/src/eval/bytes.rs index 2e0c098f..12f9bcf1 100644 --- a/crates/typst/src/eval/bytes.rs +++ b/crates/typst/src/eval/bytes.rs @@ -7,11 +7,37 @@ use comemo::Prehashed; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use crate::diag::StrResult; +use super::{cast, func, scope, ty, Array, Reflect, Str, Value}; +use crate::diag::{bail, StrResult}; -use super::Value; - -/// A shared byte buffer that is cheap to clone and hash. +/// A sequence of bytes. +/// +/// This is conceptually similar to an array of [integers]($int) between `{0}` +/// and `{255}`, but represented much more efficiently. +/// +/// You can convert +/// - a [string]($str) or an [array]($array) of integers to bytes with the +/// [`bytes`]($bytes) constructor +/// - bytes to a string with the [`str`]($str) constructor +/// - bytes to an array of integers with the [`array`]($array) constructor +/// +/// When [reading]($read) data from a file, you can decide whether to load it +/// as a string or as raw bytes. +/// +/// ```example +/// #bytes((123, 160, 22, 0)) \ +/// #bytes("Hello 😃") +/// +/// #let data = read( +/// "rhino.png", +/// encoding: none, +/// ) +/// +/// // Magic bytes. +/// #array(data.slice(0, 4)) \ +/// #str(data.slice(1, 4)) +/// ``` +#[ty(scope)] #[derive(Clone, Hash, Eq, PartialEq)] pub struct Bytes(Arc<Prehashed<Cow<'static, [u8]>>>); @@ -21,19 +47,9 @@ impl Bytes { Self(Arc::new(Prehashed::new(Cow::Borrowed(slice)))) } - /// Get the byte at the given index. - pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> { - self.locate_opt(index) - .and_then(|i| self.0.get(i).map(|&b| Value::Int(b as i64))) - .or(default) - .ok_or_else(|| out_of_bounds_no_default(index, self.len())) - } - - /// Extract a contiguous subregion of the bytes. - pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { - let start = self.locate(start)?; - let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); - Ok(self.0[start..end].into()) + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() } /// Return a view into the buffer. @@ -64,6 +80,79 @@ impl Bytes { } } +#[scope] +impl Bytes { + /// Converts a value to bytes. + /// + /// - Strings are encoded in UTF-8. + /// - Arrays of integers between `{0}` and `{255}` are converted directly. The + /// dedicated byte representation is much more efficient than the array + /// representation and thus typically used for large byte buffers (e.g. image + /// data). + /// + /// ```example + /// #bytes("Hello 😃") \ + /// #bytes((123, 160, 22, 0)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to bytes. + value: ToBytes, + ) -> Bytes { + value.0 + } + + /// The length in bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the byte at the specified index. Returns the default value if + /// the index is out of bounds or fails with an error if no default value + /// was specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the byte. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index) + .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the bytes. Fails with an error if the start or + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// is extracted. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with + /// `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Bytes> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + Ok(self.0[start..end].into()) + } +} + impl From<&[u8]> for Bytes { fn from(slice: &[u8]) -> Self { Self(Arc::new(Prehashed::new(slice.to_vec().into()))) @@ -134,6 +223,24 @@ impl Serialize for Bytes { } } +/// A value that can be cast to bytes. +pub struct ToBytes(Bytes); + +cast! { + ToBytes, + v: Str => Self(v.as_bytes().into()), + v: Array => Self(v.iter() + .map(|item| match item { + Value::Int(byte @ 0..=255) => Ok(*byte as u8), + Value::Int(_) => bail!("number must be between 0 and 255"), + value => Err(<u8 as Reflect>::error(value)), + }) + .collect::<Result<Vec<u8>, _>>()? + .into() + ), + v: Bytes => Self(v), +} + /// The out of bounds access error message. #[cold] fn out_of_bounds(index: i64, len: usize) -> EcoString { diff --git a/crates/typst/src/eval/cast.rs b/crates/typst/src/eval/cast.rs index 85cd02d4..14413a61 100644 --- a/crates/typst/src/eval/cast.rs +++ b/crates/typst/src/eval/cast.rs @@ -4,9 +4,9 @@ use unicode_math_class::MathClass; use std::fmt::Write; use std::ops::Add; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; -use super::Value; +use super::{Type, Value}; use crate::diag::{At, SourceResult, StrResult}; use crate::syntax::{Span, Spanned}; use crate::util::separated_list; @@ -25,8 +25,11 @@ use crate::util::separated_list; /// `From<T> for Value`, but that inverses the impl and leads to tons of /// `.into()` all over the place that become hard to decipher. pub trait Reflect { - /// Describe the acceptable values for this type. - fn describe() -> CastInfo; + /// Describe what can be cast into this value. + fn input() -> CastInfo; + + /// Describe what this value can be cast into. + fn output() -> CastInfo; /// Whether the given value can be converted to `T`. /// @@ -45,12 +48,16 @@ pub trait Reflect { /// ); /// ``` fn error(found: &Value) -> EcoString { - Self::describe().error(found) + Self::input().error(found) } } impl Reflect for Value { - fn describe() -> CastInfo { + fn input() -> CastInfo { + CastInfo::Any + } + + fn output() -> CastInfo { CastInfo::Any } @@ -60,8 +67,12 @@ impl Reflect for Value { } impl<T: Reflect> Reflect for Spanned<T> { - fn describe() -> CastInfo { - T::describe() + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() } fn castable(value: &Value) -> bool { @@ -70,8 +81,12 @@ impl<T: Reflect> Reflect for Spanned<T> { } impl<T: Reflect> Reflect for StrResult<T> { - fn describe() -> CastInfo { - T::describe() + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() } fn castable(value: &Value) -> bool { @@ -80,8 +95,12 @@ impl<T: Reflect> Reflect for StrResult<T> { } impl<T: Reflect> Reflect for SourceResult<T> { - fn describe() -> CastInfo { - T::describe() + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() } fn castable(value: &Value) -> bool { @@ -90,8 +109,12 @@ impl<T: Reflect> Reflect for SourceResult<T> { } impl<T: Reflect> Reflect for &T { - fn describe() -> CastInfo { - T::describe() + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() } fn castable(value: &Value) -> bool { @@ -100,8 +123,12 @@ impl<T: Reflect> Reflect for &T { } impl<T: Reflect> Reflect for &mut T { - fn describe() -> CastInfo { - T::describe() + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() } fn castable(value: &Value) -> bool { @@ -191,7 +218,7 @@ pub enum CastInfo { /// A specific value, plus short documentation for that value. Value(Value, &'static str), /// Any value of a type. - Type(&'static str), + Type(Type), /// Multiple alternatives. Union(Vec<Self>), } @@ -200,32 +227,20 @@ impl CastInfo { /// Produce an error message describing what was expected and what was /// found. pub fn error(&self, found: &Value) -> EcoString { - fn accumulate( - info: &CastInfo, - found: &Value, - parts: &mut Vec<EcoString>, - matching_type: &mut bool, - ) { - match info { - CastInfo::Any => parts.push("anything".into()), - CastInfo::Value(value, _) => { - parts.push(value.repr().into()); - if value.type_name() == found.type_name() { - *matching_type = true; - } - } - CastInfo::Type(ty) => parts.push((*ty).into()), - CastInfo::Union(options) => { - for option in options { - accumulate(option, found, parts, matching_type); - } - } - } - } - let mut matching_type = false; let mut parts = vec![]; - accumulate(self, found, &mut parts, &mut matching_type); + + self.walk(|info| match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr().into()); + if value.ty() == found.ty() { + matching_type = true; + } + } + CastInfo::Type(ty) => parts.push(eco_format!("{ty}")), + CastInfo::Union(_) => {} + }); let mut msg = String::from("expected "); if parts.is_empty() { @@ -236,7 +251,7 @@ impl CastInfo { if !matching_type { msg.push_str(", found "); - msg.push_str(found.type_name()); + write!(msg, "{}", found.ty()).unwrap(); } if_chain::if_chain! { if let Value::Int(i) = found; @@ -249,6 +264,27 @@ impl CastInfo { msg.into() } + + /// Walk all contained non-union infos. + pub fn walk<F>(&self, mut f: F) + where + F: FnMut(&Self), + { + fn inner<F>(info: &CastInfo, f: &mut F) + where + F: FnMut(&CastInfo), + { + if let CastInfo::Union(infos) = info { + for child in infos { + inner(child, f); + } + } else { + f(info); + } + } + + inner(self, &mut f) + } } impl Add for CastInfo { @@ -299,7 +335,11 @@ impl<T> Container for Vec<T> { pub enum Never {} impl Reflect for Never { - fn describe() -> CastInfo { + fn input() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn output() -> CastInfo { CastInfo::Union(vec![]) } diff --git a/crates/typst/src/eval/datetime.rs b/crates/typst/src/eval/datetime.rs index ff2d7634..6b340a8e 100644 --- a/crates/typst/src/eval/datetime.rs +++ b/crates/typst/src/eval/datetime.rs @@ -6,15 +6,114 @@ use std::ops::{Add, Sub}; use ecow::{eco_format, EcoString, EcoVec}; use time::error::{Format, InvalidFormatDescription}; -use time::{format_description, PrimitiveDateTime}; +use time::macros::format_description; +use time::{format_description, Month, PrimitiveDateTime}; -use crate::diag::bail; -use crate::eval::{Duration, StrResult}; +use super::{cast, func, scope, ty, Dict, Duration, Str, Value, Vm}; +use crate::diag::{bail, StrResult}; +use crate::geom::Smart; use crate::util::pretty_array_like; +use crate::World; -/// A datetime object that represents either a date, a time or a combination of -/// both. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +/// Represents a date, a time, or a combination of both. +/// +/// Can be created by either specifying a custom datetime using this type's +/// constructor function or getting the current date with +/// [`datetime.today`]($datetime.today). +/// +/// # Example +/// ```example +/// #let date = datetime( +/// year: 2020, +/// month: 10, +/// day: 4, +/// ) +/// +/// #date.display() \ +/// #date.display( +/// "y:[year repr:last_two]" +/// ) +/// +/// #let time = datetime( +/// hour: 18, +/// minute: 2, +/// second: 23, +/// ) +/// +/// #time.display() \ +/// #time.display( +/// "h:[hour repr:12][period]" +/// ) +/// ``` +/// +/// # Format +/// You can specify a customized formatting using the +/// [`display`]($datetime.display) method. The format of a datetime is +/// specified by providing _components_ with a specified number of _modifiers_. +/// A component represents a certain part of the datetime that you want to +/// display, and with the help of modifiers you can define how you want to +/// display that component. In order to display a component, you wrap the name +/// of the component in square brackets (e.g. `[[year]]` will display the year). +/// In order to add modifiers, you add a space after the component name followed +/// by the name of the modifier, a colon and the value of the modifier (e.g. +/// `[[month repr:short]]` will display the short representation of the month). +/// +/// The possible combination of components and their respective modifiers is as +/// follows: +/// +/// - `year`: Displays the year of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// year is padded. +/// - `repr` Can be either `full` in which case the full year is displayed or +/// `last_two` in which case only the last two digits are displayed. +/// - `sign`: Can be either `automatic` or `mandatory`. Specifies when the +/// sign should be displayed. +/// - `month`: Displays the month of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// month is padded. +/// - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the +/// month should be displayed as a number or a word. Unfortunately, when +/// choosing the word representation, it can currently only display the +/// English version. In the future, it is planned to support localization. +/// - `day`: Displays the day of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// day is padded. +/// - `week_number`: Displays the week number of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// week number is padded. +/// - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`, +/// week numbers are between 1 and 53, while the other ones are between 0 +/// and 53. +/// - `weekday`: Displays the weekday of the date. +/// - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case +/// of `long` and `short`, the corresponding English name will be displayed +/// (same as for the month, other languages are currently not supported). In +/// the case of `sunday` and `monday`, the numerical value will be displayed +/// (assuming Sunday and Monday as the first day of the week, respectively). +/// - `one_indexed`: Can be either `true` or `false`. Defines whether the +/// numerical representation of the week starts with 0 or 1. +/// - `hour`: Displays the hour of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// hour is padded. +/// - `repr`: Can be either `24` or `12`. Changes whether the hour is +/// displayed in the 24-hour or 12-hour format. +/// - `period`: The AM/PM part of the hour +/// - `case`: Can be `lower` to display it in lower case and `upper` to +/// display it in upper case. +/// - `minute`: Displays the minute of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// minute is padded. +/// - `second`: Displays the second of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// second is padded. +/// +/// Keep in mind that not always all components can be used. For example, if you +/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it +/// will be stored as a plain date internally, meaning that you cannot use +/// components such as `hour` or `minute`, which would only work on datetimes +/// that have a specified time. +#[ty(scope)] +#[derive(Clone, Copy, PartialEq, Hash)] pub enum Datetime { /// Representation as a date. Date(time::Date), @@ -25,6 +124,82 @@ pub enum Datetime { } impl Datetime { + /// Create a datetime from year, month, and day. + pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> { + Some(Datetime::Date( + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?, + )) + } + + /// Create a datetime from hour, minute, and second. + pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> { + Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?)) + } + + /// Create a datetime from day and time. + pub fn from_ymd_hms( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Option<Self> { + let date = + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?; + let time = time::Time::from_hms(hour, minute, second).ok()?; + Some(Datetime::Datetime(PrimitiveDateTime::new(date, time))) + } + + /// Try to parse a dictionary as a TOML date. + pub fn from_toml_dict(dict: &Dict) -> Option<Self> { + if dict.len() != 1 { + return None; + } + + let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else { + return None; + }; + + if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = + time::Date::parse(string, &format_description!("[year]-[month]-[day]")) + { + Self::from_ymd(d.year(), d.month() as u8, d.day()) + } else if let Ok(d) = + time::Time::parse(string, &format_description!("[hour]:[minute]:[second]")) + { + Self::from_hms(d.hour(), d.minute(), d.second()) + } else { + None + } + } + /// Which kind of variant this datetime stores. pub fn kind(&self) -> &'static str { match self { @@ -33,163 +208,274 @@ impl Datetime { Datetime::Time(_) => "time", } } +} - /// Display the date and/or time in a certain format. - pub fn display(&self, pattern: Option<EcoString>) -> Result<EcoString, EcoString> { - let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self { - Datetime::Date(_) => "[year]-[month]-[day]", - Datetime::Time(_) => "[hour]:[minute]:[second]", - Datetime::Datetime(_) => "[year]-[month]-[day] [hour]:[minute]:[second]", - }); +#[scope] +impl Datetime { + /// Creates a new datetime. + /// + /// You can specify the [datetime]($datetime) using a year, month, day, + /// hour, minute, and second. + /// + /// _Note_: Depending on which components of the datetime you specify, Typst + /// will store it in one of the following three ways: + /// * If you specify year, month and day, Typst will store just a date. + /// * If you specify hour, minute and second, Typst will store just a time. + /// * If you specify all of year, month, day, hour, minute and second, Typst + /// will store a full datetime. + /// + /// Depending on how it is stored, the [`display`]($datetime.display) method + /// will choose a different formatting by default. + /// + /// ```example + /// #datetime( + /// year: 2012, + /// month: 8, + /// day: 3, + /// ).display() + /// ``` + #[func(constructor)] + pub fn construct( + /// The year of the datetime. + #[named] + year: Option<YearComponent>, + /// The month of the datetime. + #[named] + month: Option<MonthComponent>, + /// The day of the datetime. + #[named] + day: Option<DayComponent>, + /// The hour of the datetime. + #[named] + hour: Option<HourComponent>, + /// The minute of the datetime. + #[named] + minute: Option<MinuteComponent>, + /// The second of the datetime. + #[named] + second: Option<SecondComponent>, + ) -> StrResult<Datetime> { + let time = match (hour, minute, second) { + (Some(hour), Some(minute), Some(second)) => { + match time::Time::from_hms(hour.0, minute.0, second.0) { + Ok(time) => Some(time), + Err(_) => bail!("time is invalid"), + } + } + (None, None, None) => None, + _ => bail!("time is incomplete"), + }; - let format = format_description::parse(pattern) - .map_err(format_time_invalid_format_description_error)?; + let date = match (year, month, day) { + (Some(year), Some(month), Some(day)) => { + match time::Date::from_calendar_date(year.0, month.0, day.0) { + Ok(date) => Some(date), + Err(_) => bail!("date is invalid"), + } + } + (None, None, None) => None, + _ => bail!("date is incomplete"), + }; - let formatted_result = match self { - Datetime::Date(date) => date.format(&format), - Datetime::Time(time) => time.format(&format), - Datetime::Datetime(datetime) => datetime.format(&format), - } - .map(EcoString::from); + Ok(match (date, time) { + (Some(date), Some(time)) => { + Datetime::Datetime(PrimitiveDateTime::new(date, time)) + } + (Some(date), None) => Datetime::Date(date), + (None, Some(time)) => Datetime::Time(time), + (None, None) => { + bail!("at least one of date or time must be fully specified") + } + }) + } + + /// Returns the current date. + /// + /// ```example + /// Today's date is + /// #datetime.today().display(). + /// ``` + #[func] + pub fn today( + /// The virtual machine. + vm: &mut Vm, + /// An offset to apply to the current UTC date. If set to `{auto}`, the + /// offset will be the local offset. + #[named] + #[default] + offset: Smart<i64>, + ) -> StrResult<Datetime> { + Ok(vm + .vt + .world + .today(offset.as_custom()) + .ok_or("unable to get the current date")?) + } + + /// Displays the datetime in a specified format. + /// + /// Depending on whether you have defined just a date, a time or both, the + /// default format will be different. If you specified a date, it will be + /// `[[year]-[month]-[day]]`. If you specified a time, it will be + /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be + /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`. + #[func] + pub fn display( + &self, + /// The format used to display the datetime. + #[default] + pattern: Smart<DisplayPattern>, + ) -> StrResult<EcoString> { + let pat = |s| format_description::parse_borrowed::<2>(s).unwrap(); + let result = match pattern { + Smart::Auto => match self { + Self::Date(date) => date.format(&pat("[year]-[month]-[day]")), + Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")), + Self::Datetime(datetime) => { + datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]")) + } + }, - formatted_result.map_err(format_time_format_error) + Smart::Custom(DisplayPattern(_, format)) => match self { + Self::Date(date) => date.format(&format), + Self::Time(time) => time.format(&format), + Self::Datetime(datetime) => datetime.format(&format), + }, + }; + result.map(EcoString::from).map_err(format_time_format_error) } - /// Return the year of the datetime, if existing. + /// The year if it was specified or `{none}`, otherwise. + #[func] pub fn year(&self) -> Option<i32> { match self { - Datetime::Date(date) => Some(date.year()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.year()), + Self::Date(date) => Some(date.year()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.year()), } } - /// Return the month of the datetime, if existing. + /// The month if it was specified or `{none}`, otherwise. + #[func] pub fn month(&self) -> Option<u8> { match self { - Datetime::Date(date) => Some(date.month().into()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.month().into()), + Self::Date(date) => Some(date.month().into()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.month().into()), } } - /// Return the weekday of the datetime, if existing. + /// The weekday if it was specified or `{none}`, otherwise. + #[func] pub fn weekday(&self) -> Option<u8> { match self { - Datetime::Date(date) => Some(date.weekday().number_from_monday()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), + Self::Date(date) => Some(date.weekday().number_from_monday()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), } } - /// Return the day of the datetime, if existing. + /// The day if it was specified or `{none}`, otherwise. + #[func] pub fn day(&self) -> Option<u8> { match self { - Datetime::Date(date) => Some(date.day()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.day()), + Self::Date(date) => Some(date.day()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.day()), } } - /// Return the hour of the datetime, if existing. + /// The hour if it was specified or `{none}`, otherwise. + #[func] pub fn hour(&self) -> Option<u8> { match self { - Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.hour()), - Datetime::Datetime(datetime) => Some(datetime.hour()), + Self::Date(_) => None, + Self::Time(time) => Some(time.hour()), + Self::Datetime(datetime) => Some(datetime.hour()), } } - /// Return the minute of the datetime, if existing. + /// The minute if it was specified or `{none}`, otherwise. + #[func] pub fn minute(&self) -> Option<u8> { match self { - Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.minute()), - Datetime::Datetime(datetime) => Some(datetime.minute()), + Self::Date(_) => None, + Self::Time(time) => Some(time.minute()), + Self::Datetime(datetime) => Some(datetime.minute()), } } - /// Return the second of the datetime, if existing. + /// The second if it was specified or `{none}`, otherwise. + #[func] pub fn second(&self) -> Option<u8> { match self { Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.second()), - Datetime::Datetime(datetime) => Some(datetime.second()), + Self::Time(time) => Some(time.second()), + Self::Datetime(datetime) => Some(datetime.second()), } } - /// Return the ordinal (day of the year), if existing. + /// The ordinal (day of the year), if it exists. + #[func] pub fn ordinal(&self) -> Option<u16> { match self { - Datetime::Datetime(datetime) => Some(datetime.ordinal()), - Datetime::Date(date) => Some(date.ordinal()), - Datetime::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.ordinal()), + Self::Date(date) => Some(date.ordinal()), + Self::Time(_) => None, } } +} - /// Create a datetime from year, month, and day. - pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> { - Some(Datetime::Date( - time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) - .ok()?, - )) - } - - /// Create a datetime from hour, minute, and second. - pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> { - Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?)) - } +impl Debug for Datetime { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let year = self.year().map(|y| eco_format!("year: {y}")); + let month = self.month().map(|m| eco_format!("month: {m}")); + let day = self.day().map(|d| eco_format!("day: {d}")); + let hour = self.hour().map(|h| eco_format!("hour: {h}")); + let minute = self.minute().map(|m| eco_format!("minute: {m}")); + let second = self.second().map(|s| eco_format!("second: {s}")); + let filtered = [year, month, day, hour, minute, second] + .into_iter() + .flatten() + .collect::<EcoVec<_>>(); - /// Create a datetime from day and time. - pub fn from_ymd_hms( - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Option<Self> { - let date = - time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) - .ok()?; - let time = time::Time::from_hms(hour, minute, second).ok()?; - Some(Datetime::Datetime(PrimitiveDateTime::new(date, time))) + write!(f, "datetime{}", &pretty_array_like(&filtered, false)) } } impl PartialOrd for Datetime { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { match (self, other) { - (Datetime::Datetime(a), Datetime::Datetime(b)) => a.partial_cmp(b), - (Datetime::Date(a), Datetime::Date(b)) => a.partial_cmp(b), - (Datetime::Time(a), Datetime::Time(b)) => a.partial_cmp(b), + (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b), + (Self::Date(a), Self::Date(b)) => a.partial_cmp(b), + (Self::Time(a), Self::Time(b)) => a.partial_cmp(b), _ => None, } } } impl Add<Duration> for Datetime { - type Output = Datetime; + type Output = Self; fn add(self, rhs: Duration) -> Self::Output { let rhs: time::Duration = rhs.into(); match self { - Datetime::Datetime(datetime) => Self::Datetime(datetime + rhs), - Datetime::Date(date) => Self::Date(date + rhs), - Datetime::Time(time) => Self::Time(time + rhs), + Self::Datetime(datetime) => Self::Datetime(datetime + rhs), + Self::Date(date) => Self::Date(date + rhs), + Self::Time(time) => Self::Time(time + rhs), } } } impl Sub<Duration> for Datetime { - type Output = Datetime; + type Output = Self; fn sub(self, rhs: Duration) -> Self::Output { let rhs: time::Duration = rhs.into(); match self { - Datetime::Datetime(datetime) => Self::Datetime(datetime - rhs), - Datetime::Date(date) => Self::Date(date - rhs), - Datetime::Time(time) => Self::Time(time - rhs), + Self::Datetime(datetime) => Self::Datetime(datetime - rhs), + Self::Date(date) => Self::Date(date - rhs), + Self::Time(time) => Self::Time(time - rhs), } } } @@ -199,28 +485,61 @@ impl Sub for Datetime { fn sub(self, rhs: Self) -> Self::Output { match (self, rhs) { - (Datetime::Datetime(a), Datetime::Datetime(b)) => Ok((a - b).into()), - (Datetime::Date(a), Datetime::Date(b)) => Ok((a - b).into()), - (Datetime::Time(a), Datetime::Time(b)) => Ok((a - b).into()), + (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()), + (Self::Date(a), Self::Date(b)) => Ok((a - b).into()), + (Self::Time(a), Self::Time(b)) => Ok((a - b).into()), (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()), } } } -impl Debug for Datetime { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let year = self.year().map(|y| eco_format!("year: {y}")); - let month = self.month().map(|m| eco_format!("month: {m}")); - let day = self.day().map(|d| eco_format!("day: {d}")); - let hour = self.hour().map(|h| eco_format!("hour: {h}")); - let minute = self.minute().map(|m| eco_format!("minute: {m}")); - let second = self.second().map(|s| eco_format!("second: {s}")); - let filtered = [year, month, day, hour, minute, second] - .into_iter() - .flatten() - .collect::<EcoVec<_>>(); +pub struct YearComponent(i32); +pub struct MonthComponent(Month); +pub struct DayComponent(u8); +pub struct HourComponent(u8); +pub struct MinuteComponent(u8); +pub struct SecondComponent(u8); - write!(f, "datetime{}", &pretty_array_like(&filtered, false)) +cast! { + YearComponent, + v: i32 => Self(v), +} + +cast! { + MonthComponent, + v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) +} + +cast! { + DayComponent, + v: u8 => Self(v), +} + +cast! { + HourComponent, + v: u8 => Self(v), +} + +cast! { + MinuteComponent, + v: u8 => Self(v), +} + +cast! { + SecondComponent, + v: u8 => Self(v), +} + +/// A format in which a datetime can be displayed. +pub struct DisplayPattern(Str, format_description::OwnedFormatItem); + +cast! { + DisplayPattern, + self => self.0.into_value(), + v: Str => { + let item = format_description::parse_owned::<2>(&v) + .map_err(format_time_invalid_format_description_error)?; + Self(v, item) } } diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/eval/dict.rs index a1a3948a..5f17a691 100644 --- a/crates/typst/src/eval/dict.rs +++ b/crates/typst/src/eval/dict.rs @@ -4,9 +4,10 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use super::{array, Array, Str, Value}; +use super::{array, func, scope, ty, Array, Str, Value}; use crate::diag::StrResult; use crate::syntax::is_ident; use crate::util::{pretty_array_like, separated_list, ArcExt}; @@ -26,10 +27,42 @@ macro_rules! __dict { #[doc(inline)] pub use crate::__dict as dict; -#[doc(inline)] -pub use indexmap::IndexMap; - -/// A reference-counted dictionary with value semantics. +/// A map from string keys to values. +/// +/// You can construct a dictionary by enclosing comma-separated `key: value` +/// pairs in parentheses. The values do not have to be of the same type. Since +/// empty parentheses already yield an empty array, you have to use the special +/// `(:)` syntax to create an empty dictionary. +/// +/// A dictionary is conceptually similar to an array, but it is indexed by +/// strings instead of integers. You can access and create dictionary entries +/// with the `.at()` method. If you know the key statically, you can +/// alternatively use [field access notation]($scripting/#fields) (`.key`) to +/// access the value. Dictionaries can be added with the `+` operator and +/// [joined together]($scripting/#blocks). To check whether a key is present in +/// the dictionary, use the `in` keyword. +/// +/// You can iterate over the pairs in a dictionary using a [for +/// loop]($scripting/#loops). This will iterate in the order the pairs were +/// inserted / declared. +/// +/// # Example +/// ```example +/// #let dict = ( +/// name: "Typst", +/// born: 2019, +/// ) +/// +/// #dict.name \ +/// #(dict.launch = 20) +/// #dict.len() \ +/// #dict.keys() \ +/// #dict.values() \ +/// #dict.at("born") \ +/// #dict.insert("city", "Berlin ") +/// #("name" in dict) +/// ``` +#[ty(scope, name = "dictionary")] #[derive(Default, Clone, PartialEq)] pub struct Dict(Arc<IndexMap<Str, Value>>); @@ -44,18 +77,14 @@ impl Dict { self.0.is_empty() } - /// The number of pairs in the dictionary. - pub fn len(&self) -> usize { - self.0.len() + /// Borrow the value at the given key. + pub fn get(&self, key: &str) -> StrResult<&Value> { + self.0.get(key).ok_or_else(|| missing_key(key)) } - /// Borrow the value the given `key` maps to, - pub fn at(&self, key: &str, default: Option<Value>) -> StrResult<Value> { - self.0 - .get(key) - .cloned() - .or(default) - .ok_or_else(|| missing_key_no_default(key)) + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult<Value> { + Arc::make_mut(&mut self.0).remove(key).ok_or_else(|| missing_key(key)) } /// Mutably borrow the value the given `key` maps to. @@ -65,31 +94,11 @@ impl Dict { .ok_or_else(|| missing_key_no_default(key)) } - /// Remove the value if the dictionary contains the given key. - pub fn take(&mut self, key: &str) -> StrResult<Value> { - Arc::make_mut(&mut self.0) - .remove(key) - .ok_or_else(|| eco_format!("missing key: {:?}", Str::from(key))) - } - /// Whether the dictionary contains a specific key. pub fn contains(&self, key: &str) -> bool { self.0.contains_key(key) } - /// Insert a mapping from the given `key` to the given `value`. - pub fn insert(&mut self, key: Str, value: Value) { - Arc::make_mut(&mut self.0).insert(key, value); - } - - /// Remove a mapping by `key` and return the value. - pub fn remove(&mut self, key: &str) -> StrResult<Value> { - match Arc::make_mut(&mut self.0).shift_remove(key) { - Some(value) => Ok(value), - None => Err(missing_key(key)), - } - } - /// Clear the dictionary. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { @@ -99,25 +108,6 @@ impl Dict { } } - /// Return the keys of the dictionary as an array. - pub fn keys(&self) -> Array { - self.0.keys().cloned().map(Value::Str).collect() - } - - /// Return the values of the dictionary as an array. - pub fn values(&self) -> Array { - self.0.values().cloned().collect() - } - - /// Return the values of the dictionary as an array of pairs (arrays of - /// length two). - pub fn pairs(&self) -> Array { - self.0 - .iter() - .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) - .collect() - } - /// Iterate over pairs of references to the contained keys and values. pub fn iter(&self) -> indexmap::map::Iter<Str, Value> { self.0.iter() @@ -135,6 +125,80 @@ impl Dict { } } +#[scope] +impl Dict { + /// The number of pairs in the dictionary. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the value associated with the specified key in the dictionary. + /// May be used on the left-hand side of an assignment if the key is already + /// present in the dictionary. Returns the default value if the key is not + /// part of the dictionary or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The key at which to retrieve the item. + key: Str, + /// A default value to return if the key is not part of the dictionary. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.0 + .get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(&key)) + } + + /// Insert a new pair into the dictionary and return the value. If the + /// dictionary already contains this key, the value is updated. + #[func] + pub fn insert( + &mut self, + /// The key of the pair that should be inserted. + key: Str, + /// The value of the pair that should be inserted. + value: Value, + ) { + Arc::make_mut(&mut self.0).insert(key, value); + } + + /// Remove a pair from the dictionary by key and return the value. + #[func] + pub fn remove(&mut self, key: Str) -> StrResult<Value> { + match Arc::make_mut(&mut self.0).shift_remove(&key) { + Some(value) => Ok(value), + None => Err(missing_key(&key)), + } + } + + /// Returns the keys of the dictionary as an array in insertion order. + #[func] + pub fn keys(&self) -> Array { + self.0.keys().cloned().map(Value::Str).collect() + } + + /// Returns the values of the dictionary as an array in insertion order. + #[func] + pub fn values(&self) -> Array { + self.0.values().cloned().collect() + } + + /// Returns the keys and values of the dictionary as an array of pairs. Each + /// pair is represented as an array of length two. + #[func] + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) + .collect() + } +} + impl Debug for Dict { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if self.is_empty() { diff --git a/crates/typst/src/eval/duration.rs b/crates/typst/src/eval/duration.rs index 500ce209..1bbf8492 100644 --- a/crates/typst/src/eval/duration.rs +++ b/crates/typst/src/eval/duration.rs @@ -1,11 +1,14 @@ -use crate::util::pretty_array_like; use ecow::eco_format; use std::fmt; use std::fmt::{Debug, Formatter}; use std::ops::{Add, Div, Mul, Neg, Sub}; use time::ext::NumericalDuration; +use super::{func, scope, ty}; +use crate::util::pretty_array_like; + /// Represents a positive or negative span of time. +#[ty(scope)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Duration(time::Duration); @@ -14,39 +17,100 @@ impl Duration { pub fn is_zero(&self) -> bool { self.0.is_zero() } +} + +#[scope] +impl Duration { + /// Creates a new duration. + /// + /// You can specify the [duration]($duration) using weeks, days, hours, + /// minutes and seconds. You can also get a duration by subtracting two + /// [datetimes]($datetime). + /// + /// ```example + /// #duration( + /// days: 3, + /// hours: 12, + /// ).hours() + /// ``` + #[func(constructor)] + pub fn construct( + /// The number of seconds. + #[named] + #[default(0)] + seconds: i64, + /// The number of minutes. + #[named] + #[default(0)] + minutes: i64, + /// The number of hours. + #[named] + #[default(0)] + hours: i64, + /// The number of days. + #[named] + #[default(0)] + days: i64, + /// The number of weeks. + #[named] + #[default(0)] + weeks: i64, + ) -> Duration { + Duration::from( + time::Duration::seconds(seconds) + + time::Duration::minutes(minutes) + + time::Duration::hours(hours) + + time::Duration::days(days) + + time::Duration::weeks(weeks), + ) + } /// The duration expressed in seconds. + /// + /// This function returns the total duration represented in seconds as a + /// floating-point number rather than the second component of the duration. + #[func] pub fn seconds(&self) -> f64 { self.0.as_seconds_f64() } /// The duration expressed in minutes. + /// + /// This function returns the total duration represented in minutes as a + /// floating-point number rather than the second component of the duration. + #[func] pub fn minutes(&self) -> f64 { self.seconds() / 60.0 } /// The duration expressed in hours. + /// + /// This function returns the total duration represented in hours as a + /// floating-point number rather than the second component of the duration. + #[func] pub fn hours(&self) -> f64 { self.seconds() / 3_600.0 } /// The duration expressed in days. + /// + /// This function returns the total duration represented in days as a + /// floating-point number rather than the second component of the duration. + #[func] pub fn days(&self) -> f64 { self.seconds() / 86_400.0 } /// The duration expressed in weeks. + /// + /// This function returns the total duration represented in weeks as a + /// floating-point number rather than the second component of the duration. + #[func] pub fn weeks(&self) -> f64 { self.seconds() / 604_800.0 } } -impl From<time::Duration> for Duration { - fn from(value: time::Duration) -> Self { - Self(value) - } -} - impl Debug for Duration { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let mut tmp = self.0; @@ -85,6 +149,12 @@ impl Debug for Duration { } } +impl From<time::Duration> for Duration { + fn from(value: time::Duration) -> Self { + Self(value) + } +} + impl From<Duration> for time::Duration { fn from(value: Duration) -> Self { value.0 diff --git a/crates/typst/src/eval/fields.rs b/crates/typst/src/eval/fields.rs index 8c00873b..094cfa38 100644 --- a/crates/typst/src/eval/fields.rs +++ b/crates/typst/src/eval/fields.rs @@ -1,22 +1,23 @@ use ecow::{eco_format, EcoString}; use crate::diag::StrResult; -use crate::geom::{Axes, GenAlign, PartialStroke, Stroke}; +use crate::geom::{Align, Length, Rel, Stroke}; -use super::{IntoValue, Value}; +use super::{IntoValue, Type, Value}; /// Try to access a field on a value. -/// This function is exclusively for types which have -/// predefined fields, such as stroke and length. +/// +/// This function is exclusively for types which have predefined fields, such as +/// stroke and length. pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> { - let name = value.type_name(); - let not_supported = || Err(no_fields(name)); - let missing = || Err(missing_field(name, field)); + let ty = value.ty(); + let nope = || Err(no_fields(ty)); + let missing = || Err(missing_field(ty, field)); // Special cases, such as module and dict, are handled by Value itself let result = match value { Value::Length(length) => match field { - "em" => length.em.into_value(), + "em" => length.em.get().into_value(), "abs" => length.abs.into_value(), _ => return missing(), }, @@ -26,44 +27,27 @@ pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> { _ => return missing(), }, Value::Dyn(dynamic) => { - if let Some(stroke) = dynamic.downcast::<PartialStroke>() { + if let Some(stroke) = dynamic.downcast::<Stroke>() { match field { - "paint" => stroke - .paint - .clone() - .unwrap_or_else(|| Stroke::default().paint) - .into_value(), - "thickness" => stroke - .thickness - .unwrap_or_else(|| Stroke::default().thickness.into()) - .into_value(), - "cap" => stroke - .line_cap - .unwrap_or_else(|| Stroke::default().line_cap) - .into_value(), - "join" => stroke - .line_join - .unwrap_or_else(|| Stroke::default().line_join) - .into_value(), - "dash" => stroke.dash_pattern.clone().unwrap_or(None).into_value(), - "miter-limit" => stroke - .miter_limit - .unwrap_or_else(|| Stroke::default().miter_limit) - .0 - .into_value(), + "paint" => stroke.paint.clone().into_value(), + "thickness" => stroke.thickness.into_value(), + "cap" => stroke.line_cap.into_value(), + "join" => stroke.line_join.into_value(), + "dash" => stroke.dash_pattern.clone().into_value(), + "miter-limit" => stroke.miter_limit.map(|limit| limit.0).into_value(), _ => return missing(), } - } else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() { + } else if let Some(align) = dynamic.downcast::<Align>() { match field { - "x" => align2d.x.into_value(), - "y" => align2d.y.into_value(), + "x" => align.x().into_value(), + "y" => align.y().into_value(), _ => return missing(), } } else { - return not_supported(); + return nope(); } } - _ => return not_supported(), + _ => return nope(), }; Ok(result) @@ -71,23 +55,27 @@ pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> { /// The error message for a type not supporting field access. #[cold] -fn no_fields(type_name: &str) -> EcoString { - eco_format!("cannot access fields on type {type_name}") +fn no_fields(ty: Type) -> EcoString { + eco_format!("cannot access fields on type {ty}") } /// The missing field error message. #[cold] -fn missing_field(type_name: &str, field: &str) -> EcoString { - eco_format!("{type_name} does not contain field \"{field}\"") +fn missing_field(ty: Type, field: &str) -> EcoString { + eco_format!("{ty} does not contain field \"{field}\"") } /// List the available fields for a type. -pub fn fields_on(type_name: &str) -> &[&'static str] { - match type_name { - "length" => &["em", "abs"], - "relative length" => &["ratio", "length"], - "stroke" => &["paint", "thickness", "cap", "join", "dash", "miter-limit"], - "2d alignment" => &["x", "y"], - _ => &[], +pub fn fields_on(ty: Type) -> &'static [&'static str] { + if ty == Type::of::<Length>() { + &["em", "abs"] + } else if ty == Type::of::<Rel>() { + &["ratio", "length"] + } else if ty == Type::of::<Stroke>() { + &["paint", "thickness", "cap", "join", "dash", "miter-limit"] + } else if ty == Type::of::<Align>() { + &["x", "y"] + } else { + &[] } } diff --git a/crates/typst/src/eval/float.rs b/crates/typst/src/eval/float.rs new file mode 100644 index 00000000..094ff1db --- /dev/null +++ b/crates/typst/src/eval/float.rs @@ -0,0 +1,60 @@ +use ecow::eco_format; + +use super::{cast, func, scope, ty, Str}; +use crate::geom::Ratio; + +/// A floating-point number. +/// +/// A limited-precision representation of a real number. Typst uses 64 bits to +/// store floats. Wherever a float is expected, you can also pass an +/// [integer]($int). +/// +/// You can convert a value to a float with this type's constructor. +/// +/// # Example +/// ```example +/// #3.14 \ +/// #1e4 \ +/// #(10 / 4) +/// ``` +#[ty(scope, name = "float")] +type f64; + +#[scope] +impl f64 { + /// Converts a value to a float. + /// + /// - Booleans are converted to `0.0` or `1.0`. + /// - Integers are converted to the closest 64-bit float. + /// - Ratios are divided by 100%. + /// - Strings are parsed in base 10 to the closest 64-bit float. + /// Exponential notation is supported. + /// + /// ```example + /// #float(false) \ + /// #float(true) \ + /// #float(4) \ + /// #float(40%) \ + /// #float("2.7") \ + /// #float("1e5") + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a float. + value: ToFloat, + ) -> f64 { + value.0 + } +} + +/// A value that can be cast to a float. +pub struct ToFloat(f64); + +cast! { + ToFloat, + v: bool => Self(v as i64 as f64), + v: i64 => Self(v as f64), + v: Ratio => Self(v.get()), + v: Str => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?), + v: f64 => Self(v), +} diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs index effafa7b..45a1efbd 100644 --- a/crates/typst/src/eval/func.rs +++ b/crates/typst/src/eval/func.rs @@ -1,22 +1,123 @@ use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::sync::Arc; use comemo::{Prehashed, Tracked, TrackedMut}; -use ecow::eco_format; use once_cell::sync::Lazy; use super::{ - cast, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, Tracer, - Value, Vm, + cast, scope, ty, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, + Tracer, Type, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; -use crate::model::{DelayedErrors, ElemFunc, Introspector, Locator, Vt}; +use crate::model::{ + Content, DelayedErrors, Element, Introspector, Locator, Selector, Vt, +}; use crate::syntax::ast::{self, AstNode}; use crate::syntax::{FileId, Span, SyntaxNode}; +use crate::util::Static; use crate::World; -/// An evaluatable function. +#[doc(inline)] +pub use typst_macros::func; + +/// A mapping from argument values to a return value. +/// +/// You can call a function by writing a comma-separated list of function +/// _arguments_ enclosed in parentheses directly after the function name. +/// Additionally, you can pass any number of trailing content blocks arguments +/// to a function _after_ the normal argument list. If the normal argument list +/// would become empty, it can be omitted. Typst supports positional and named +/// arguments. The former are identified by position and type, while the later +/// are written as `name: value`. +/// +/// Within math mode, function calls have special behaviour. See the +/// [math documentation]($category/math) for more details. +/// +/// # Example +/// ```example +/// // Call a function. +/// #list([A], [B]) +/// +/// // Named arguments and trailing +/// // content blocks. +/// #enum(start: 2)[A][B] +/// +/// // Version without parentheses. +/// #list[A][B] +/// ``` +/// +/// Functions are a fundamental building block of Typst. Typst provides +/// functions for a variety of typesetting tasks. Moreover, the markup you write +/// is backed by functions and all styling happens through functions. This +/// reference lists all available functions and how you can use them. Please +/// also refer to the documentation about [set]($styling/#set-rules) and +/// [show]($styling/#show-rules) rules to learn about additional ways you can +/// work with functions in Typst. +/// +/// # Element functions +/// Some functions are associated with _elements_ like [headings]($heading) or +/// [tables]($table). When called, these create an element of their respective +/// kind. In contrast to normal functions, they can further be used in [set +/// rules]($styling/#set-rules), [show rules]($styling/#show-rules), and +/// [selectors]($selector). +/// +/// # Function scopes +/// Functions can hold related definitions in their own scope, similar to a +/// [module]($scripting/#modules). Examples of this are +/// [`assert.eq`]($assert.eq) or [`list.item`]($list.item). However, this +/// feature is currently only available for built-in functions. +/// +/// # Defining functions +/// You can define your own function with a [let binding]($scripting/#bindings) +/// that has a parameter list after the binding's name. The parameter list can +/// contain positional parameters, named parameters with default values and +/// [argument sinks]($arguments). The right-hand side of the binding can be a +/// block or any other expression. It defines the function's return value and +/// can depend on the parameters. +/// +/// ```example +/// #let alert(body, fill: red) = { +/// set text(white) +/// set align(center) +/// rect( +/// fill: fill, +/// inset: 8pt, +/// radius: 4pt, +/// [*Warning:\ #body*], +/// ) +/// } +/// +/// #alert[ +/// Danger is imminent! +/// ] +/// +/// #alert(fill: blue)[ +/// KEEP OFF TRACKS +/// ] +/// ``` +/// +/// # Unnamed functions { #unnamed } +/// You can also created an unnamed function without creating a binding by +/// specifying a parameter list followed by `=>` and the function body. If your +/// function has just one parameter, the parentheses around the parameter list +/// are optional. Unnamed functions are mainly useful for show rules, but also +/// for settable properties that take functions like the page function's +/// [`footer`]($page.footer) property. +/// +/// ```example +/// #show "once?": it => [#it #it] +/// once? +/// ``` +/// +/// # Notable fact +/// In Typst, all functions are _pure._ This means that for the same +/// arguments, they always return the same result. They cannot "remember" things to +/// produce another value when they are called a second time. +/// +/// The only exception are built-in methods like +/// [`array.push(value)`]($array.push). These can modify the values they are +/// called on. +#[ty(scope, name = "function")] #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] pub struct Func { @@ -30,9 +131,9 @@ pub struct Func { #[derive(Clone, PartialEq, Hash)] enum Repr { /// A native Rust function. - Native(&'static NativeFunc), + Native(Static<NativeFuncData>), /// A function for an element. - Elem(ElemFunc), + Element(Element), /// A user-defined closure. Closure(Arc<Prehashed<Closure>>), /// A nested function with pre-applied arguments. @@ -40,37 +141,106 @@ enum Repr { } impl Func { - /// The name of the function. + /// The function's name (e.g. `min`). + /// + /// Returns `None` if this is an anonymous closure. pub fn name(&self) -> Option<&str> { match &self.repr { - Repr::Native(native) => Some(native.info.name), - Repr::Elem(func) => Some(func.info().name), + Repr::Native(native) => Some(native.name), + Repr::Element(elem) => Some(elem.name()), Repr::Closure(closure) => closure.name(), - Repr::With(arc) => arc.0.name(), + Repr::With(with) => with.0.name(), } } - /// Extract details the function. - pub fn info(&self) -> Option<&FuncInfo> { + /// The function's title case name, for use in documentation (e.g. `Minimum`). + /// + /// Returns `None` if this is a closure. + pub fn title(&self) -> Option<&'static str> { match &self.repr { - Repr::Native(native) => Some(&native.info), - Repr::Elem(func) => Some(func.info()), + Repr::Native(native) => Some(native.title), + Repr::Element(elem) => Some(elem.title()), Repr::Closure(_) => None, - Repr::With(arc) => arc.0.info(), + Repr::With(with) => with.0.title(), } } - /// The function's span. - pub fn span(&self) -> Span { - self.span + /// Documentation for the function (as Markdown). + pub fn docs(&self) -> Option<&'static str> { + match &self.repr { + Repr::Native(native) => Some(native.docs), + Repr::Element(elem) => Some(elem.docs()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.docs(), + } } - /// Attach a span to this function if it doesn't already have one. - pub fn spanned(mut self, span: Span) -> Self { - if self.span.is_detached() { - self.span = span; + /// Get details about this function's parameters if available. + pub fn params(&self) -> Option<&'static [ParamInfo]> { + match &self.repr { + Repr::Native(native) => Some(&native.0.params), + Repr::Element(elem) => Some(elem.params()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.params(), + } + } + + /// Get the parameter info for a parameter with the given name if it exist. + pub fn param(&self, name: &str) -> Option<&'static ParamInfo> { + self.params()?.iter().find(|param| param.name == name) + } + + /// Get details about the function's return type. + pub fn returns(&self) -> Option<&'static CastInfo> { + static CONTENT: Lazy<CastInfo> = + Lazy::new(|| CastInfo::Type(Type::of::<Content>())); + match &self.repr { + Repr::Native(native) => Some(&native.0.returns), + Repr::Element(_) => Some(&CONTENT), + Repr::Closure(_) => None, + Repr::With(with) => with.0.returns(), + } + } + + /// Search keywords for the function. + pub fn keywords(&self) -> &'static [&'static str] { + match &self.repr { + Repr::Native(native) => native.keywords, + Repr::Element(elem) => elem.keywords(), + Repr::Closure(_) => &[], + Repr::With(with) => with.0.keywords(), + } + } + + /// The function's associated scope of sub-definition. + pub fn scope(&self) -> Option<&'static Scope> { + match &self.repr { + Repr::Native(native) => Some(&native.0.scope), + Repr::Element(elem) => Some(elem.scope()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.scope(), + } + } + + /// Get a field from this function's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + let scope = + self.scope().ok_or("cannot access fields on user-defined functions")?; + match scope.get(field) { + Some(field) => Ok(field), + None => match self.name() { + Some(name) => bail!("function `{name}` does not contain field `{field}`"), + None => bail!("function does not contain field `{field}`"), + }, + } + } + + /// Extract the element function, if it is one. + pub fn element(&self) -> Option<Element> { + match self.repr { + Repr::Element(func) => Some(func), + _ => None, } - self } /// Call the function with the given arguments. @@ -83,11 +253,11 @@ impl Func { match &self.repr { Repr::Native(native) => { - let value = (native.func)(vm, &mut args)?; + let value = (native.function)(vm, &mut args)?; args.finish()?; Ok(value) } - Repr::Elem(func) => { + Repr::Element(func) => { let value = func.construct(vm, &mut args)?; args.finish()?; Ok(Value::Content(value)) @@ -109,9 +279,9 @@ impl Func { args, ) } - Repr::With(arc) => { - args.items = arc.1.items.iter().cloned().chain(args.items).collect(); - arc.0.call_vm(vm, args) + Repr::With(with) => { + args.items = with.1.items.iter().cloned().chain(args.items).collect(); + with.0.call_vm(vm, args) } } } @@ -138,42 +308,56 @@ impl Func { self.call_vm(&mut vm, args) } - /// Apply the given arguments to the function. - pub fn with(self, args: Args) -> Self { - let span = self.span; - Self { repr: Repr::With(Arc::new((self, args))), span } + /// The function's span. + pub fn span(&self) -> Span { + self.span } - /// Extract the element function, if it is one. - pub fn element(&self) -> Option<ElemFunc> { - match self.repr { - Repr::Elem(func) => Some(func), - _ => None, + /// Attach a span to this function if it doesn't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; } + self } +} - /// Get a field from this function's scope, if possible. - pub fn get(&self, field: &str) -> StrResult<&Value> { - match &self.repr { - Repr::Native(func) => func.info.scope.get(field).ok_or_else(|| { - eco_format!( - "function `{}` does not contain field `{}`", - func.info.name, - field - ) - }), - Repr::Elem(func) => func.info().scope.get(field).ok_or_else(|| { - eco_format!( - "function `{}` does not contain field `{}`", - func.name(), - field - ) - }), - Repr::Closure(_) => { - Err(eco_format!("cannot access fields on user-defined functions")) - } - Repr::With(arc) => arc.0.get(field), - } +#[scope] +impl Func { + /// Returns a new function that has the given arguments pre-applied. + #[func] + pub fn with( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: Args, + /// The arguments to apply to the function. + #[external] + arguments: Args, + ) -> Func { + let span = self.span; + Self { repr: Repr::With(Arc::new((self, args))), span } + } + + /// Returns a selector that filters for elements belonging to this function + /// whose fields have the values of the given arguments. + #[func] + pub fn where_( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: Args, + /// The fields to filter for. + #[external] + fields: Args, + ) -> StrResult<Selector> { + let mut args = args; + let fields = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + Ok(self + .element() + .ok_or("`where()` can only be called on element functions")? + .where_(fields)) } } @@ -198,82 +382,56 @@ impl From<Repr> for Func { } } -impl From<ElemFunc> for Func { - fn from(func: ElemFunc) -> Self { - Repr::Elem(func).into() +impl From<Element> for Func { + fn from(func: Element) -> Self { + Repr::Element(func).into() } } -/// A Typst function defined by a native Rust function. -pub struct NativeFunc { - /// The function's implementation. - pub func: fn(&mut Vm, &mut Args) -> SourceResult<Value>, - /// Details about the function. - pub info: Lazy<FuncInfo>, -} - -impl PartialEq for NativeFunc { - fn eq(&self, other: &Self) -> bool { - self.func as usize == other.func as usize +/// A Typst function that is defined by a native Rust type that shadows a +/// native Rust function. +pub trait NativeFunc { + /// Get the function for the native Rust type. + fn func() -> Func { + Func::from(Self::data()) } -} - -impl Eq for NativeFunc {} -impl Hash for NativeFunc { - fn hash<H: Hasher>(&self, state: &mut H) { - (self.func as usize).hash(state); - } + /// Get the function data for the native Rust type. + fn data() -> &'static NativeFuncData; } -impl From<&'static NativeFunc> for Func { - fn from(native: &'static NativeFunc) -> Self { - Repr::Native(native).into() - } -} - -cast! { - &'static NativeFunc, - self => Value::Func(self.into()), -} - -/// Details about a function. -#[derive(Debug, Clone)] -pub struct FuncInfo { - /// The function's name. +/// Defines a native function. +pub struct NativeFuncData { + pub function: fn(&mut Vm, &mut Args) -> SourceResult<Value>, pub name: &'static str, - /// The display name of the function. - pub display: &'static str, - /// A string of search keywords. - pub keywords: Option<&'static str>, - /// Which category the function is part of. - pub category: &'static str, - /// Documentation for the function. + pub title: &'static str, pub docs: &'static str, - /// Details about the function's parameters. - pub params: Vec<ParamInfo>, - /// Valid values for the return value. - pub returns: CastInfo, - /// The function's own scope of fields and sub-functions. - pub scope: Scope, + pub keywords: &'static [&'static str], + pub scope: Lazy<Scope>, + pub params: Lazy<Vec<ParamInfo>>, + pub returns: Lazy<CastInfo>, } -impl FuncInfo { - /// Get the parameter info for a parameter with the given name - pub fn param(&self, name: &str) -> Option<&ParamInfo> { - self.params.iter().find(|param| param.name == name) +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() } } -/// Describes a named parameter. +cast! { + &'static NativeFuncData, + self => Func::from(self).into_value(), +} + +/// Describes a function parameter. #[derive(Debug, Clone)] pub struct ParamInfo { /// The parameter's name. pub name: &'static str, /// Documentation for the parameter. pub docs: &'static str, - /// Valid values for the parameter. - pub cast: CastInfo, + /// Describe what values this parameter accepts. + pub input: CastInfo, /// Creates an instance of the parameter's default value. pub default: Option<fn() -> Value>, /// Is the parameter positional? diff --git a/crates/typst/src/eval/int.rs b/crates/typst/src/eval/int.rs index 4e081617..55c709c0 100644 --- a/crates/typst/src/eval/int.rs +++ b/crates/typst/src/eval/int.rs @@ -1,12 +1,72 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize}; -use super::{cast, Value}; +use ecow::eco_format; + +use super::{cast, func, scope, ty, Str, Value}; + +/// A whole number. +/// +/// The number can be negative, zero, or positive. As Typst uses 64 bits to +/// store integers, integers cannot be smaller than `{-9223372036854775808}` or +/// larger than `{9223372036854775807}`. +/// +/// The number can also be specified as hexadecimal, octal, or binary by +/// starting it with a zero followed by either `x`, `o`, or `b`. +/// +/// You can convert a value to an integer with this type's constructor. +/// +/// # Example +/// ```example +/// #(1 + 2) \ +/// #(2 - 5) \ +/// #(3 + 4 < 8) +/// +/// #0xff \ +/// #0o10 \ +/// #0b1001 +/// ``` +#[ty(scope, name = "int", title = "Integer")] +type i64; + +#[scope] +impl i64 { + /// Converts a value to an integer. + /// + /// - Booleans are converted to `0` or `1`. + /// - Floats are floored to the next 64-bit integer. + /// - Strings are parsed in base 10. + /// + /// ```example + /// #int(false) \ + /// #int(true) \ + /// #int(2.7) \ + /// #(int("27") + int("4")) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an integer. + value: ToInt, + ) -> i64 { + value.0 + } +} + +/// A value that can be cast to an integer. +pub struct ToInt(i64); + +cast! { + ToInt, + v: bool => Self(v as i64), + v: f64 => Self(v as i64), + v: Str => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?), + v: i64 => Self(v), +} macro_rules! signed_int { ($($ty:ty)*) => { $(cast! { $ty, - self => Value::Int(self as i64), + self => Value::Int(self as _), v: i64 => v.try_into().map_err(|_| "number too large")?, })* } @@ -16,7 +76,7 @@ macro_rules! unsigned_int { ($($ty:ty)*) => { $(cast! { $ty, - self => Value::Int(self as i64), + self => Value::Int(self as _), v: i64 => v.try_into().map_err(|_| { if v < 0 { "number must be at least zero" @@ -28,54 +88,55 @@ macro_rules! unsigned_int { } } -macro_rules! signed_nonzero { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self.get() as i64), - v: i64 => v - .try_into() - .ok() - .and_then($ty::new) - .ok_or_else(|| if v == 0 { - "number must not be zero" - } else { - "number too large" - })?, - })* - } -} - -macro_rules! unsigned_nonzero { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self.get() as i64), - v: i64 => v - .try_into() - .ok() - .and_then($ty::new) - .ok_or_else(|| if v <= 0 { - "number must be positive" - } else { - "number too large" - })?, - })* - } -} +signed_int! { i8 i16 i32 isize } +unsigned_int! { u8 u16 u32 u64 usize } -signed_int! { - i8 i16 i32 isize +cast! { + NonZeroI64, + self => Value::Int(self.get() as _), + v: i64 => v.try_into() + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, } -unsigned_int! { - u8 u16 u32 u64 usize +cast! { + NonZeroIsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: isize| v.try_into()) + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, } -signed_nonzero! { - NonZeroI64 NonZeroIsize +cast! { + NonZeroU64, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: u64| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, } -unsigned_nonzero! { - NonZeroU64 NonZeroUsize +cast! { + NonZeroUsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: usize| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, } diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs index 78ae7a59..5d65e193 100644 --- a/crates/typst/src/eval/library.rs +++ b/crates/typst/src/eval/library.rs @@ -6,12 +6,11 @@ use comemo::Tracked; use ecow::EcoString; use std::sync::OnceLock; -use super::{Args, Dynamic, Module, NativeFunc, Value, Vm}; +use super::Module; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; -use crate::model::{Content, ElemFunc, Introspector, Label, StyleChain, Styles, Vt}; -use crate::syntax::Span; +use crate::model::{Content, Element, Introspector, Label, StyleChain, Styles, Vt}; use crate::util::hash128; /// Definition of Typst's standard library. @@ -43,8 +42,8 @@ pub struct LangItems { pub linebreak: fn() -> Content, /// Plain text without markup. pub text: fn(text: EcoString) -> Content, - /// The text function. - pub text_func: ElemFunc, + /// The text element. + pub text_elem: Element, /// Get the string if this is a text element. pub text_str: fn(&Content) -> Option<EcoString>, /// A smart quote: `'` or `"`. @@ -69,20 +68,14 @@ pub struct LangItems { fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, - /// The heading function. - pub heading_func: ElemFunc, + /// The heading element. + pub heading_elem: Element, /// An item in a bullet list: `- ...`. pub list_item: fn(body: Content) -> Content, /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. pub enum_item: fn(number: Option<usize>, body: Content) -> Content, /// An item in a term list: `/ Term: Details`. pub term_item: fn(term: Content, description: Content) -> Content, - /// The constructor for the 'rgba' color kind. - pub rgb_func: &'static NativeFunc, - /// The constructor for the 'cmyk' color kind. - pub cmyk_func: &'static NativeFunc, - /// The constructor for the 'luma' color kind. - pub luma_func: &'static NativeFunc, /// A mathematical equation: `$x$`, `$ x^2 $`. pub equation: fn(body: Content, block: bool) -> Content, /// An alignment point in math: `&`. @@ -110,14 +103,6 @@ pub struct LangItems { pub math_frac: fn(num: Content, denom: Content) -> Content, /// A root in math: `√x`, `∛x` or `∜x`. pub math_root: fn(index: Option<Content>, radicand: Content) -> Content, - /// Dispatch a method on a library value. - pub library_method: fn( - vm: &mut Vm, - dynamic: &Dynamic, - method: &str, - args: Args, - span: Span, - ) -> SourceResult<Value>, } impl Debug for LangItems { @@ -134,7 +119,7 @@ impl Hash for LangItems { self.space.hash(state); self.linebreak.hash(state); self.text.hash(state); - self.text_func.hash(state); + self.text_elem.hash(state); (self.text_str as usize).hash(state); self.smart_quote.hash(state); self.parbreak.hash(state); @@ -146,13 +131,10 @@ impl Hash for LangItems { self.reference.hash(state); (self.bibliography_keys as usize).hash(state); self.heading.hash(state); - self.heading_func.hash(state); + self.heading_elem.hash(state); self.list_item.hash(state); self.enum_item.hash(state); self.term_item.hash(state); - self.rgb_func.hash(state); - self.cmyk_func.hash(state); - self.luma_func.hash(state); self.equation.hash(state); self.math_align_point.hash(state); self.math_delimited.hash(state); @@ -160,7 +142,6 @@ impl Hash for LangItems { self.math_accent.hash(state); self.math_frac.hash(state); self.math_root.hash(state); - (self.library_method as usize).hash(state); } } diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs index 85f87cc7..b8d71c76 100644 --- a/crates/typst/src/eval/methods.rs +++ b/crates/typst/src/eval/methods.rs @@ -1,323 +1,36 @@ -//! Methods on values. +//! Handles special built-in methods on values. -use ecow::{eco_format, EcoString}; - -use super::{Args, Bytes, IntoValue, Plugin, Str, Value, Vm}; -use crate::diag::{At, Hint, SourceResult}; -use crate::eval::bail; -use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign}; -use crate::model::{Location, Selector}; +use super::{Args, Array, Dict, Str, Type, Value}; +use crate::diag::{At, SourceResult}; use crate::syntax::Span; -/// Call a method on a value. -pub fn call( - vm: &mut Vm, - value: Value, - method: &str, - mut args: Args, - span: Span, -) -> SourceResult<Value> { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); - - let output = match value { - Value::Color(color) => match method { - "lighten" => color.lighten(args.expect("amount")?).into_value(), - "darken" => color.darken(args.expect("amount")?).into_value(), - "negate" => color.negate().into_value(), - "kind" => match color { - Color::Luma(_) => vm.items.luma_func.into_value(), - Color::Rgba(_) => vm.items.rgb_func.into_value(), - Color::Cmyk(_) => vm.items.cmyk_func.into_value(), - }, - "hex" => color.to_rgba().to_hex().into_value(), - "rgba" => color.to_rgba().to_array().into_value(), - "cmyk" => match color { - Color::Luma(luma) => luma.to_cmyk().to_array().into_value(), - Color::Rgba(_) => { - bail!(span, "cannot obtain cmyk values from rgba color") - } - Color::Cmyk(cmyk) => cmyk.to_array().into_value(), - }, - "luma" => match color { - Color::Luma(luma) => luma.0.into_value(), - Color::Rgba(_) => { - bail!(span, "cannot obtain the luma value of rgba color") - } - Color::Cmyk(_) => { - bail!(span, "cannot obtain the luma value of cmyk color") - } - }, - _ => return missing(), - }, - - Value::Str(string) => match method { - "len" => string.len().into_value(), - "first" => string.first().at(span)?.into_value(), - "last" => string.last().at(span)?.into_value(), - "at" => string - .at(args.expect("index")?, args.named("default")?) - .at(span)? - .into_value(), - "slice" => { - let start = args.expect("start")?; - let mut end = args.eat()?; - if end.is_none() { - end = args.named("count")?.map(|c: i64| start + c); - } - string.slice(start, end).at(span)?.into_value() - } - "clusters" => string.clusters().into_value(), - "codepoints" => string.codepoints().into_value(), - "contains" => string.contains(args.expect("pattern")?).into_value(), - "starts-with" => string.starts_with(args.expect("pattern")?).into_value(), - "ends-with" => string.ends_with(args.expect("pattern")?).into_value(), - "find" => string.find(args.expect("pattern")?).into_value(), - "position" => string.position(args.expect("pattern")?).into_value(), - "match" => string.match_(args.expect("pattern")?).into_value(), - "matches" => string.matches(args.expect("pattern")?).into_value(), - "replace" => { - let pattern = args.expect("pattern")?; - let with = args.expect("string or function")?; - let count = args.named("count")?; - string.replace(vm, pattern, with, count)?.into_value() - } - "rev" => string.rev().into_value(), - "trim" => { - let pattern = args.eat()?; - let at = args.named("at")?; - let repeat = args.named("repeat")?.unwrap_or(true); - string.trim(pattern, at, repeat).into_value() - } - "split" => string.split(args.eat()?).into_value(), - _ => return missing(), - }, - - Value::Bytes(bytes) => match method { - "len" => bytes.len().into_value(), - "at" => bytes.at(args.expect("index")?, args.named("default")?).at(span)?, - "slice" => { - let start = args.expect("start")?; - let mut end = args.eat()?; - if end.is_none() { - end = args.named("count")?.map(|c: i64| start + c); - } - bytes.slice(start, end).at(span)?.into_value() - } - _ => return missing(), - }, - - Value::Datetime(datetime) => match method { - "display" => datetime.display(args.eat()?).at(args.span)?.into_value(), - "year" => datetime.year().into_value(), - "month" => datetime.month().into_value(), - "weekday" => datetime.weekday().into_value(), - "day" => datetime.day().into_value(), - "hour" => datetime.hour().into_value(), - "minute" => datetime.minute().into_value(), - "second" => datetime.second().into_value(), - "ordinal" => datetime.ordinal().into_value(), - _ => return missing(), - }, - - Value::Duration(duration) => match method { - "seconds" => duration.seconds().into_value(), - "minutes" => duration.minutes().into_value(), - "hours" => duration.hours().into_value(), - "days" => duration.days().into_value(), - "weeks" => duration.weeks().into_value(), - _ => return missing(), - }, - - Value::Content(content) => match method { - "func" => content.func().into_value(), - "has" => content.has(&args.expect::<EcoString>("field")?).into_value(), - "at" => content - .at(&args.expect::<Str>("field")?, args.named("default")?) - .at(span)?, - "fields" => content.dict().into_value(), - "location" => content - .location() - .ok_or("this method can only be called on content returned by query(..)") - .at(span)? - .into_value(), - _ => return missing(), - }, - - Value::Array(array) => match method { - "len" => array.len().into_value(), - "first" => array.first().at(span)?.clone(), - "last" => array.last().at(span)?.clone(), - "at" => array.at(args.expect("index")?, args.named("default")?).at(span)?, - "slice" => { - let start = args.expect("start")?; - let mut end = args.eat()?; - if end.is_none() { - end = args.named("count")?.map(|c: i64| start + c); - } - array.slice(start, end).at(span)?.into_value() - } - "contains" => array.contains(&args.expect("value")?).into_value(), - "find" => array.find(vm, args.expect("function")?)?.into_value(), - "position" => array.position(vm, args.expect("function")?)?.into_value(), - "filter" => array.filter(vm, args.expect("function")?)?.into_value(), - "map" => array.map(vm, args.expect("function")?)?.into_value(), - "fold" => { - array.fold(vm, args.expect("initial value")?, args.expect("function")?)? - } - "sum" => array.sum(args.named("default")?, span)?, - "product" => array.product(args.named("default")?, span)?, - "any" => array.any(vm, args.expect("function")?)?.into_value(), - "all" => array.all(vm, args.expect("function")?)?.into_value(), - "flatten" => array.flatten().into_value(), - "rev" => array.rev().into_value(), - "split" => array.split(args.expect("separator")?).into_value(), - "join" => { - let sep = args.eat()?; - let last = args.named("last")?; - array.join(sep, last).at(span)? - } - "intersperse" => array.intersperse(args.expect("separator")?).into_value(), - "sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(), - "zip" => array.zip(&mut args)?.into_value(), - "enumerate" => array - .enumerate(args.named("start")?.unwrap_or(0)) - .at(span)? - .into_value(), - "dedup" => array.dedup(vm, args.named("key")?)?.into_value(), - _ => return missing(), - }, - - Value::Dict(dict) => match method { - "len" => dict.len().into_value(), - "at" => dict - .at(&args.expect::<Str>("key")?, args.named("default")?) - .at(span)?, - "keys" => dict.keys().into_value(), - "values" => dict.values().into_value(), - "pairs" => dict.pairs().into_value(), - _ => { - return if matches!(dict.at(method, None), Ok(Value::Func(_))) { - Err(missing_method(name, method)) - .hint(eco_format!( - "to call the function stored in the dictionary, surround the field access with parentheses" - )) - .at(span) - } else { - missing() - } - } - }, - - Value::Func(func) => match method { - "with" => func.with(args.take()).into_value(), - "where" => { - let fields = args.to_named(); - args.items.retain(|arg| arg.name.is_none()); - func.element() - .ok_or("`where()` can only be called on element functions") - .at(span)? - .where_(fields) - .into_value() - } - _ => return missing(), - }, - - Value::Length(length) => match method { - unit @ ("pt" | "cm" | "mm" | "inches") => { - if length.em != Em::zero() { - return Err(eco_format!("cannot convert a length with non-zero em units ({length:?}) to {unit}")) - .hint(eco_format!("use 'length.abs.{unit}()' instead to ignore its em component")) - .at(span); - } - match unit { - "pt" => length.abs.to_pt().into_value(), - "cm" => length.abs.to_cm().into_value(), - "mm" => length.abs.to_mm().into_value(), - "inches" => length.abs.to_inches().into_value(), - _ => unreachable!(), - } - } - _ => return missing(), - }, - - Value::Angle(angle) => match method { - "deg" => angle.to_deg().into_value(), - "rad" => angle.to_rad().into_value(), - _ => return missing(), - }, - - Value::Args(args) => match method { - "pos" => args.to_pos().into_value(), - "named" => args.to_named().into_value(), - _ => return missing(), - }, - - Value::Dyn(dynamic) => { - if let Some(location) = dynamic.downcast::<Location>() { - match method { - "page" => vm.vt.introspector.page(*location).into_value(), - "position" => vm.vt.introspector.position(*location).into_value(), - "page-numbering" => vm.vt.introspector.page_numbering(*location), - _ => return missing(), - } - } else if let Some(selector) = dynamic.downcast::<Selector>() { - match method { - "or" => selector.clone().or(args.all::<Selector>()?).into_value(), - "and" => selector.clone().and(args.all::<Selector>()?).into_value(), - "before" => { - let location = args.expect::<Selector>("selector")?; - let inclusive = - args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().before(location, inclusive).into_value() - } - "after" => { - let location = args.expect::<Selector>("selector")?; - let inclusive = - args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().after(location, inclusive).into_value() - } - _ => return missing(), - } - } else if let Some(direction) = dynamic.downcast::<Dir>() { - match method { - "axis" => direction.axis().description().into_value(), - "start" => { - GenAlign::from(Align::from(direction.start())).into_value() - } - "end" => GenAlign::from(Align::from(direction.end())).into_value(), - "inv" => direction.inv().into_value(), - _ => return missing(), - } - } else if let Some(align) = dynamic.downcast::<GenAlign>() { - match method { - "axis" => align.axis().description().into_value(), - "inv" => align.inv().into_value(), - _ => return missing(), - } - } else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() { - match method { - "inv" => align2d.map(GenAlign::inv).into_value(), - _ => return missing(), - } - } else if let Some(plugin) = dynamic.downcast::<Plugin>() { - if plugin.iter().any(|func_name| func_name == method) { - let bytes = args.all::<Bytes>()?; - args.take().finish()?; - plugin.call(method, bytes).at(span)?.into_value() - } else { - return missing(); - } - } else { - return (vm.items.library_method)(vm, &dynamic, method, args, span); - } - } +/// Whether a specific method is mutating. +pub fn is_mutating(method: &str) -> bool { + matches!(method, "push" | "pop" | "insert" | "remove") +} - _ => return missing(), - }; +/// Whether a specific method is an accessor. +pub fn is_accessor(method: &str) -> bool { + matches!(method, "first" | "last" | "at") +} - args.finish()?; - Ok(output) +/// List the available methods for a type and whether they take arguments. +pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] { + if ty == Type::of::<Array>() { + &[ + ("first", false), + ("last", false), + ("at", true), + ("pop", false), + ("push", true), + ("insert", true), + ("remove", true), + ] + } else if ty == Type::of::<Dict>() { + &[("at", true), ("insert", true), ("remove", true)] + } else { + &[] + } } /// Call a mutating method on a value. @@ -327,8 +40,8 @@ pub fn call_mut( mut args: Args, span: Span, ) -> SourceResult<Value> { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); + let ty = value.ty(); + let missing = || Err(missing_method(ty, method)).at(span); let mut output = Value::None; match value { @@ -344,9 +57,7 @@ pub fn call_mut( Value::Dict(dict) => match method { "insert" => dict.insert(args.expect::<Str>("key")?, args.expect("value")?), - "remove" => { - output = dict.remove(&args.expect::<EcoString>("key")?).at(span)? - } + "remove" => output = dict.remove(args.expect::<Str>("key")?).at(span)?, _ => return missing(), }, @@ -364,8 +75,8 @@ pub fn call_access<'a>( mut args: Args, span: Span, ) -> SourceResult<&'a mut Value> { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); + let ty = value.ty(); + let missing = || Err(missing_method(ty, method)).at(span); let slot = match value { Value::Array(array) => match method { @@ -385,134 +96,8 @@ pub fn call_access<'a>( Ok(slot) } -/// Whether a specific method is mutating. -pub fn is_mutating(method: &str) -> bool { - matches!(method, "push" | "pop" | "insert" | "remove") -} - -/// Whether a specific method is an accessor. -pub fn is_accessor(method: &str) -> bool { - matches!(method, "first" | "last" | "at") -} - /// The missing method error message. #[cold] -fn missing_method(type_name: &str, method: &str) -> String { - format!("type {type_name} has no method `{method}`") -} - -/// List the available methods for a type and whether they take arguments. -pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { - match type_name { - "color" => &[ - ("lighten", true), - ("darken", true), - ("negate", false), - ("kind", false), - ("hex", false), - ("rgba", false), - ("cmyk", false), - ("luma", false), - ], - "string" => &[ - ("len", false), - ("at", true), - ("clusters", false), - ("codepoints", false), - ("contains", true), - ("ends-with", true), - ("find", true), - ("first", false), - ("last", false), - ("match", true), - ("matches", true), - ("position", true), - ("replace", true), - ("slice", true), - ("split", true), - ("starts-with", true), - ("trim", true), - ], - "bytes" => &[("len", false), ("at", true), ("slice", true)], - "datetime" => &[ - ("display", true), - ("year", false), - ("month", false), - ("weekday", false), - ("day", false), - ("hour", false), - ("minute", false), - ("second", false), - ("ordinal", false), - ], - "duration" => &[ - ("seconds", false), - ("minutes", false), - ("hours", false), - ("days", false), - ("weeks", false), - ], - "content" => &[ - ("func", false), - ("has", true), - ("at", true), - ("fields", false), - ("location", false), - ], - "array" => &[ - ("all", true), - ("any", true), - ("at", true), - ("contains", true), - ("filter", true), - ("find", true), - ("first", false), - ("flatten", false), - ("fold", true), - ("insert", true), - ("split", true), - ("join", true), - ("last", false), - ("len", false), - ("map", true), - ("pop", false), - ("position", true), - ("push", true), - ("remove", true), - ("rev", false), - ("slice", true), - ("sorted", false), - ("enumerate", false), - ("zip", true), - ], - "dictionary" => &[ - ("at", true), - ("insert", true), - ("keys", false), - ("len", false), - ("pairs", false), - ("remove", true), - ("values", false), - ], - "function" => &[("where", true), ("with", true)], - "length" => &[("pt", false), ("cm", false), ("mm", false), ("inches", false)], - "angle" => &[("deg", false), ("rad", false)], - "arguments" => &[("named", false), ("pos", false)], - "location" => &[("page", false), ("position", false), ("page-numbering", false)], - "selector" => &[("or", true), ("and", true), ("before", true), ("after", true)], - "direction" => { - &[("axis", false), ("start", false), ("end", false), ("inv", false)] - } - "alignment" => &[("axis", false), ("inv", false)], - "2d alignment" => &[("inv", false)], - "counter" => &[ - ("display", true), - ("at", true), - ("final", true), - ("step", true), - ("update", true), - ], - "state" => &[("display", true), ("at", true), ("final", true), ("update", true)], - _ => &[], - } +fn missing_method(ty: Type, method: &str) -> String { + format!("type {ty} has no method `{method}`") } diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index a6fd5e17..cbd00bc1 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -14,10 +14,12 @@ mod str; mod value; mod args; mod auto; +mod bool; mod bytes; mod datetime; mod duration; mod fields; +mod float; mod func; mod int; mod methods; @@ -28,6 +30,7 @@ mod plugin; mod scope; mod symbol; mod tracer; +mod ty; #[doc(hidden)] pub use { @@ -37,9 +40,6 @@ pub use { once_cell::sync::Lazy, }; -#[doc(inline)] -pub use typst_macros::{func, symbols}; - pub use self::args::{Arg, Args}; pub use self::array::{array, Array}; pub use self::auto::AutoValue; @@ -50,18 +50,20 @@ pub use self::cast::{ pub use self::datetime::Datetime; pub use self::dict::{dict, Dict}; pub use self::duration::Duration; -pub use self::fields::fields_on; -pub use self::func::{Func, FuncInfo, NativeFunc, ParamInfo}; +pub use self::func::{func, Func, NativeFunc, NativeFuncData, ParamInfo}; pub use self::library::{set_lang_items, LangItems, Library}; -pub use self::methods::methods_on; pub use self::module::Module; pub use self::none::NoneValue; pub use self::plugin::Plugin; -pub use self::scope::{Scope, Scopes}; +pub use self::scope::{NativeScope, Scope, Scopes}; pub use self::str::{format_str, Regex, Str}; -pub use self::symbol::Symbol; +pub use self::symbol::{symbols, Symbol}; pub use self::tracer::Tracer; -pub use self::value::{Dynamic, Type, Value}; +pub use self::ty::{scope, ty, NativeType, NativeTypeData, Type}; +pub use self::value::{Dynamic, Value}; + +pub(crate) use self::fields::fields_on; +pub(crate) use self::methods::mutable_methods_on; use std::collections::HashSet; use std::mem; @@ -150,7 +152,7 @@ pub fn eval( .unwrap_or_default() .to_string_lossy(); - Ok(Module::new(name).with_scope(vm.scopes.top).with_content(output)) + Ok(Module::new(name, vm.scopes.top).with_content(output)) } /// Evaluate a string as code and return the resulting value. @@ -575,7 +577,7 @@ impl Eval for ast::Escape<'_> { #[tracing::instrument(name = "Escape::eval", skip_all)] fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Symbol(Symbol::new(self.get()))) + Ok(Value::Symbol(Symbol::single(self.get()))) } } @@ -584,7 +586,7 @@ impl Eval for ast::Shorthand<'_> { #[tracing::instrument(name = "Shorthand::eval", skip_all)] fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Symbol(Symbol::new(self.get()))) + Ok(Value::Symbol(Symbol::single(self.get()))) } } @@ -992,7 +994,7 @@ impl Eval for ast::Array<'_> { ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { Value::None => {} Value::Array(array) => vec.extend(array.into_iter()), - v => bail!(expr.span(), "cannot spread {} into array", v.type_name()), + v => bail!(expr.span(), "cannot spread {} into array", v.ty()), }, } } @@ -1019,11 +1021,7 @@ impl Eval for ast::Dict<'_> { ast::DictItem::Spread(expr) => match expr.eval(vm)? { Value::None => {} Value::Dict(dict) => map.extend(dict.into_iter()), - v => bail!( - expr.span(), - "cannot spread {} into dictionary", - v.type_name() - ), + v => bail!(expr.span(), "cannot spread {} into dictionary", v.ty()), }, } } @@ -1085,8 +1083,8 @@ fn apply_binary_expr( let lhs = binary.lhs().eval(vm)?; // Short-circuit boolean operations. - if (binary.op() == ast::BinOp::And && lhs == Value::Bool(false)) - || (binary.op() == ast::BinOp::Or && lhs == Value::Bool(true)) + if (binary.op() == ast::BinOp::And && lhs == false.into_value()) + || (binary.op() == ast::BinOp::Or && lhs == true.into_value()) { return Ok(lhs); } @@ -1146,49 +1144,83 @@ impl Eval for ast::FuncCall<'_> { let callee_span = callee.span(); let args = self.args(); - // Try to evaluate as a method call. This is possible if the callee is a - // field access and does not evaluate to a module. + // Try to evaluate as a call to an associated function or field. let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { let target = access.target(); + let target_span = target.span(); let field = access.field(); - let point = || Tracepoint::Call(Some(field.get().clone())); - if methods::is_mutating(&field) { - let args = args.eval(vm)?; + let field_span = field.span(); + + let target = if methods::is_mutating(&field) { + let mut args = args.eval(vm)?; let target = target.access(vm)?; - // Prioritize a function's own methods (with, where) over its - // fields. This is fine as we define each field of a function, - // if it has any. - // ('methods_on' will be empty for Symbol and Module - their - // method calls always refer to their fields.) - if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) - || methods_on(target.type_name()) - .iter() - .any(|&(m, _)| m == field.as_str()) - { + // Only arrays and dictionaries have mutable methods. + if matches!(target, Value::Array(_) | Value::Dict(_)) { + args.span = span; + let point = || Tracepoint::Call(Some(field.get().clone())); return methods::call_mut(target, &field, args, span).trace( vm.world(), point, span, ); } - (target.field(&field).at(field.span())?, args) + + target.clone() } else { - let target = target.eval(vm)?; - let args = args.eval(vm)?; - - if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) - || methods_on(target.type_name()) - .iter() - .any(|&(m, _)| m == field.as_str()) - { - return methods::call(vm, target, &field, args, span).trace( - vm.world(), - point, - span, - ); + access.target().eval(vm)? + }; + + let mut args = args.eval(vm)?; + + // Handle plugins. + if let Value::Plugin(plugin) = &target { + let bytes = args.all::<Bytes>()?; + args.finish()?; + return Ok(plugin.call(&field, bytes).at(span)?.into_value()); + } + + // Prioritize associated functions on the value's type (i.e., + // methods) over its fields. A function call on a field is only + // allowed for functions, types, modules (because they are scopes), + // and symbols (because they have modifiers). + // + // For dictionaries, it is not allowed because it would be ambigious + // (prioritizing associated functions would make an addition of a + // new associated function a breaking change and prioritizing fields + // would break associated functions for certain dictionaries). + if let Some(callee) = target.ty().scope().get(&field) { + let this = Arg { + span: target_span, + name: None, + value: Spanned::new(target, target_span), + }; + args.span = span; + args.items.insert(0, this); + (callee.clone(), args) + } else if matches!( + target, + Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) + ) { + (target.field(&field).at(field_span)?, args) + } else { + let mut error = error!( + field_span, + "type {} has no method `{}`", + target.ty(), + field.as_str() + ); + + if let Value::Dict(dict) = target { + if matches!(dict.get(&field), Ok(Value::Func(_))) { + error.hint( + "to call the function stored in the dictionary, \ + surround the field access with parentheses", + ); + } } - (target.field(&field).at(field.span())?, args) + + bail!(error); } } else { (callee.eval(vm)?, args.eval(vm)?) @@ -1284,7 +1316,7 @@ impl Eval for ast::Args<'_> { })); } Value::Args(args) => items.extend(args.items), - v => bail!(expr.span(), "cannot spread {}", v.type_name()), + v => bail!(expr.span(), "cannot spread {}", v.ty()), }, } } @@ -1364,7 +1396,7 @@ where ast::Pattern::Destructuring(destruct) => match value { Value::Array(value) => destructure_array(vm, pattern, value, f, destruct)?, Value::Dict(value) => destructure_dict(vm, value, f, destruct)?, - _ => bail!(pattern.span(), "cannot destructure {}", value.type_name()), + _ => bail!(pattern.span(), "cannot destructure {}", value.ty()), }, } Ok(()) @@ -1436,21 +1468,15 @@ where for p in destruct.bindings() { match p { ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { - let v = dict - .at(&ident, None) - .map_err(|_| "destructuring key not found in dictionary") - .at(ident.span())?; - f(vm, ast::Expr::Ident(ident), v)?; + let v = dict.get(&ident).at(ident.span())?; + f(vm, ast::Expr::Ident(ident), v.clone())?; used.insert(ident.as_str()); } ast::DestructuringKind::Sink(spread) => sink = spread.expr(), ast::DestructuringKind::Named(named) => { let name = named.name(); - let v = dict - .at(&name, None) - .map_err(|_| "destructuring key not found in dictionary") - .at(name.span())?; - f(vm, named.expr(), v)?; + let v = dict.get(&name).at(name.span())?; + f(vm, named.expr(), v.clone())?; used.insert(name.as_str()); } ast::DestructuringKind::Placeholder(_) => {} @@ -1690,10 +1716,10 @@ impl Eval for ast::ForLoop<'_> { iter!(for pattern in array); } (ast::Pattern::Normal(_), _) => { - bail!(self.iter().span(), "cannot loop over {}", iter.type_name()); + bail!(self.iter().span(), "cannot loop over {}", iter.ty()); } (_, _) => { - bail!(pattern.span(), "cannot destructure values of {}", iter.type_name()) + bail!(pattern.span(), "cannot destructure values of {}", iter.ty()) } } @@ -1789,16 +1815,25 @@ impl Eval for ast::ModuleImport<'_> { } } if let Value::Func(func) = source { - if func.info().is_none() { + let Some(scope) = func.scope() else { bail!(span, "cannot import from user-defined functions"); - } + }; apply_imports( self.imports(), vm, func, new_name, - |func| func.info().unwrap().name.into(), - |func| &func.info().unwrap().scope, + |func| func.name().unwrap_or_default().into(), + |_| scope, + )?; + } else if let Value::Type(ty) = source { + apply_imports( + self.imports(), + vm, + ty, + new_name, + |ty| ty.short_name().into(), + |ty| ty.scope(), )?; } else { let module = import(vm, source, span, true)?; @@ -1833,16 +1868,16 @@ fn import( vm: &mut Vm, source: Value, span: Span, - accept_functions: bool, + allow_scopes: bool, ) -> SourceResult<Module> { let path = match source { Value::Str(path) => path, Value::Module(module) => return Ok(module), v => { - if accept_functions { - bail!(span, "expected path, module or function, found {}", v.type_name()) + if allow_scopes { + bail!(span, "expected path, module, function, or type, found {}", v.ty()) } else { - bail!(span, "expected path or module, found {}", v.type_name()) + bail!(span, "expected path or module, found {}", v.ty()) } } }; @@ -2038,20 +2073,22 @@ fn access_dict<'a>( match access.target().access(vm)? { Value::Dict(dict) => Ok(dict), value => { - let type_name = value.type_name(); + let ty = value.ty(); let span = access.target().span(); if matches!( value, // those types have their own field getters Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_) ) { - bail!(span, "cannot mutate fields on {type_name}"); - } else if fields::fields_on(type_name).is_empty() { - bail!(span, "{type_name} does not have accessible fields"); + bail!(span, "cannot mutate fields on {ty}"); + } else if fields::fields_on(ty).is_empty() { + bail!(span, "{ty} does not have accessible fields"); } else { // type supports static fields, which don't yet have // setters - Err(eco_format!("fields on {type_name} are not yet mutable")) - .hint(eco_format!("try creating a new {type_name} with the updated field value instead")) + Err(eco_format!("fields on {ty} are not yet mutable")) + .hint(eco_format!( + "try creating a new {ty} with the updated field value instead" + )) .at(span) } } diff --git a/crates/typst/src/eval/module.rs b/crates/typst/src/eval/module.rs index 0bc6bf38..9f02948c 100644 --- a/crates/typst/src/eval/module.rs +++ b/crates/typst/src/eval/module.rs @@ -3,12 +3,27 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; -use super::{Content, Scope, Value}; +use super::{ty, Content, Scope, Value}; use crate::diag::StrResult; -/// An evaluated module, ready for importing or typesetting. +/// An evaluated module, either built-in or resulting from a file. /// -/// Values of this type are cheap to clone and hash. +/// You can access definitions from the module using [field access +/// notation]($scripting/#fields) and interact with it using the [import and +/// include syntaxes]($scripting/#modules). +/// +/// # Example +/// ```example +/// <<< #import "utils.typ" +/// <<< #utils.add(2, 5) +/// +/// <<< #import utils: sub +/// <<< #sub(1, 4) +/// >>> #7 +/// >>> +/// >>> #(-3) +/// ``` +#[ty] #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] pub struct Module { @@ -29,10 +44,10 @@ struct Repr { impl Module { /// Create a new module. - pub fn new(name: impl Into<EcoString>) -> Self { + pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self { Self { name: name.into(), - inner: Arc::new(Repr { scope: Scope::new(), content: Content::empty() }), + inner: Arc::new(Repr { scope, content: Content::empty() }), } } @@ -70,7 +85,7 @@ impl Module { } /// Try to access a definition in the module. - pub fn get(&self, name: &str) -> StrResult<&Value> { + pub fn field(&self, name: &str) -> StrResult<&Value> { self.scope().get(name).ok_or_else(|| { eco_format!("module `{}` does not contain `{name}`", self.name()) }) diff --git a/crates/typst/src/eval/none.rs b/crates/typst/src/eval/none.rs index ab7644a7..5262301f 100644 --- a/crates/typst/src/eval/none.rs +++ b/crates/typst/src/eval/none.rs @@ -1,15 +1,33 @@ use std::fmt::{self, Debug, Formatter}; -use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value}; +use serde::{Serialize, Serializer}; + +use super::{cast, ty, CastInfo, FromValue, IntoValue, Reflect, Type, Value}; use crate::diag::StrResult; /// A value that indicates the absence of any other value. +/// +/// The none type has exactly one value: `{none}`. +/// +/// When inserted into the document, it is not visible. This is also the value +/// that is produced by empty code blocks. It can be +/// [joined]($scripting/#blocks) with any value, yielding the other value. +/// +/// # Example +/// ```example +/// Not visible: #none +/// ``` +#[ty(name = "none")] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct NoneValue; impl Reflect for NoneValue { - fn describe() -> CastInfo { - CastInfo::Type("none") + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) } fn castable(value: &Value) -> bool { @@ -38,6 +56,15 @@ impl Debug for NoneValue { } } +impl Serialize for NoneValue { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_none() + } +} + cast! { (), self => Value::None, @@ -45,8 +72,12 @@ cast! { } impl<T: Reflect> Reflect for Option<T> { - fn describe() -> CastInfo { - T::describe() + NoneValue::describe() + fn input() -> CastInfo { + T::input() + NoneValue::input() + } + + fn output() -> CastInfo { + T::output() + NoneValue::output() } fn castable(value: &Value) -> bool { diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs index 2dc3c8f9..e44c3a41 100644 --- a/crates/typst/src/eval/ops.rs +++ b/crates/typst/src/eval/ops.rs @@ -5,15 +5,15 @@ use std::fmt::Debug; use ecow::eco_format; -use super::{format_str, Regex, Value}; +use super::{format_str, IntoValue, Regex, Value}; use crate::diag::{bail, StrResult}; -use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; +use crate::geom::{Align, Length, Numeric, Rel, Smart, Stroke}; use Value::*; /// Bail with a type mismatch error. macro_rules! mismatch { ($fmt:expr, $($value:expr),* $(,)?) => { - return Err(eco_format!($fmt, $($value.type_name()),*)) + return Err(eco_format!($fmt, $($value.ty()),*)) }; } @@ -55,7 +55,7 @@ pub fn pos(value: Value) -> StrResult<Value> { /// Compute the negation of a value. pub fn neg(value: Value) -> StrResult<Value> { Ok(match value { - Int(v) => Int(v.checked_neg().ok_or("value is too large")?), + Int(v) => Int(v.checked_neg().ok_or_else(too_large)?), Float(v) => Float(-v), Length(v) => Length(-v), Angle(v) => Angle(-v), @@ -73,7 +73,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { (a, None) => a, (None, b) => b, - (Int(a), Int(b)) => Int(a.checked_add(b).ok_or("value is too large")?), + (Int(a), Int(b)) => Int(a.checked_add(b).ok_or_else(too_large)?), (Int(a), Float(b)) => Float(a as f64 + b), (Float(a), Int(b)) => Float(a + b as f64), (Float(a), Float(b)) => Float(a + b), @@ -108,32 +108,22 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), - (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { - Value::dynamic(PartialStroke { - paint: Smart::Custom(color.into()), - thickness: Smart::Custom(thickness), - ..PartialStroke::default() - }) + (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => Stroke { + paint: Smart::Custom(color.into()), + thickness: Smart::Custom(thickness), + ..Stroke::default() } + .into_value(), (Duration(a), Duration(b)) => Duration(a + b), (Datetime(a), Duration(b)) => Datetime(a + b), (Duration(a), Datetime(b)) => Datetime(b + a), (Dyn(a), Dyn(b)) => { - // 1D alignments can be summed into 2D alignments. - if let (Some(&a), Some(&b)) = - (a.downcast::<GenAlign>(), b.downcast::<GenAlign>()) - { - if a.axis() == b.axis() { - return Err(eco_format!("cannot add two {:?} alignments", a.axis())); - } - - return Ok(Value::dynamic(match a.axis() { - Axis::X => Axes { x: a, y: b }, - Axis::Y => Axes { x: b, y: a }, - })); - }; + // Alignments can be summed. + if let (Some(&a), Some(&b)) = (a.downcast::<Align>(), b.downcast::<Align>()) { + return Ok((a + b)?.into_value()); + } mismatch!("cannot add {} and {}", a, b); } @@ -145,7 +135,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the difference of two values. pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { Ok(match (lhs, rhs) { - (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or("value is too large")?), + (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(too_large)?), (Int(a), Float(b)) => Float(a as f64 - b), (Float(a), Int(b)) => Float(a - b as f64), (Float(a), Float(b)) => Float(a - b), @@ -177,7 +167,7 @@ pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the product of two values. pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { Ok(match (lhs, rhs) { - (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or("value is too large")?), + (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(too_large)?), (Int(a), Float(b)) => Float(a as f64 * b), (Float(a), Int(b)) => Float(a * b as f64), (Float(a), Float(b)) => Float(a * b), @@ -216,10 +206,10 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { (Float(a), Fraction(b)) => Fraction(a * b), (Ratio(a), Fraction(b)) => Fraction(a.get() * b), - (Str(a), Int(b)) => Str(a.repeat(b)?), - (Int(a), Str(b)) => Str(b.repeat(a)?), - (Array(a), Int(b)) => Array(a.repeat(b)?), - (Int(a), Array(b)) => Array(b.repeat(a)?), + (Str(a), Int(b)) => Str(a.repeat(Value::Int(b).cast()?)?), + (Int(a), Str(b)) => Str(b.repeat(Value::Int(a).cast()?)?), + (Array(a), Int(b)) => Array(a.repeat(Value::Int(b).cast()?)?), + (Int(a), Array(b)) => Array(b.repeat(Value::Int(a).cast()?)?), (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), @@ -375,7 +365,9 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Dict(a), Dict(b)) => a == b, (Func(a), Func(b)) => a == b, (Args(a), Args(b)) => a == b, + (Type(a), Type(b)) => a == b, (Module(a), Module(b)) => a == b, + (Plugin(a), Plugin(b)) => a == b, (Datetime(a), Datetime(b)) => a == b, (Duration(a), Duration(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, @@ -456,7 +448,12 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), (Str(a), Dict(b)) => Some(b.contains(a)), - (a, Array(b)) => Some(b.contains(a)), + (a, Array(b)) => Some(b.contains(a.clone())), _ => Option::None, } } + +#[cold] +fn too_large() -> &'static str { + "value is too large" +} diff --git a/crates/typst/src/eval/plugin.rs b/crates/typst/src/eval/plugin.rs index 7dbc8b3e..29ca79c0 100644 --- a/crates/typst/src/eval/plugin.rs +++ b/crates/typst/src/eval/plugin.rs @@ -5,14 +5,105 @@ use ecow::{eco_format, EcoString}; use std::sync::{Arc, Mutex}; use wasmi::{AsContext, AsContextMut, Caller, Engine, Linker, Module}; -use super::{cast, Bytes}; -use crate::diag::{bail, StrResult}; +use super::{func, scope, ty, Bytes, Vm}; +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::syntax::Spanned; +use crate::World; -/// A plugin loaded from WebAssembly code. +/// A WebAssembly plugin. /// -/// It can run external code conforming to its protocol. +/// This is **advanced functionality** and not to be confused with +/// [Typst packages]($scripting/#packages). /// -/// This type is cheap to clone and hash. +/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin +/// functions may accept multiple [byte buffers]($bytes) as arguments and return +/// a single byte buffer. They should typically be wrapped in idiomatic Typst +/// functions that perform the necessary conversions between native Typst types +/// and bytes. +/// +/// Plugins run in isolation from your system, which means that printing, +/// reading files, or anything like that will not be supported for security +/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit +/// shared WebAssembly library. Many compilers will use the +/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. +/// emscripten), which allows printing, reading files, etc. This ABI will not +/// directly work with Typst. You will either need to compile to a different +/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/blob/master/wasi-stub). +/// +/// # Example +/// ```example +/// #let myplugin = plugin("hello.wasm") +/// #let concat(a, b) = str( +/// myplugin.concatenate( +/// bytes(a), +/// bytes(b), +/// ) +/// ) +/// +/// #concat("hello", "world") +/// ``` +/// +/// # Protocol +/// To be used as a plugin, a WebAssembly module must conform to the following +/// protocol: +/// +/// ## Exports +/// A plugin module can export functions to make them callable from Typst. To +/// conform to the protocol, an exported function should: +/// +/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as +/// lengths, so `usize/size_t` may be preferable), and return one 32-bit +/// integer. +/// +/// - The function should first allocate a buffer `buf` of length +/// `a_1 + a_2 + ... + a_n`, and then call +/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. +/// +/// - The `a_1` first bytes of the buffer now constitute the first argument, the +/// `a_2` next bytes the second argument, and so on. +/// +/// - The function can now do its job with the arguments and produce an output +/// buffer. Before returning, it should call +/// `wasm_minimal_protocol_send_result_to_host` to send its result back to the +/// host. +/// +/// - To signal success, the function should return `0`. +/// +/// - To signal an error, the function should return `1`. The written buffer is +/// then interpreted as an UTF-8 encoded error message. +/// +/// ## Imports +/// Plugin modules need to import two functions that are provided by the runtime. +/// (Types and functions are described using WAT syntax.) +/// +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// +/// Writes the arguments for the current function into a plugin-allocated +/// buffer. When a plugin function is called, it +/// [receives the lengths](#exports) of its input buffers as arguments. It +/// should then allocate a buffer whose capacity is at least the sum of these +/// lengths. It should then call this function with a `ptr` to the buffer to +/// fill it with the arguments, one after another. +/// +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// +/// Sends the output of the current function to the host (Typst). The first +/// parameter shall be a pointer to a buffer (`ptr`), while the second is the +/// length of that buffer (`len`). The memory pointed at by `ptr` can be freed +/// immediately after this function returns. If the message should be +/// interpreted as an error message, it should be encoded as UTF-8. +/// +/// # Resources +/// For more resources, check out the +/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). +/// It contains: +/// +/// - A list of example plugin implementations and a test runner for these +/// examples +/// - Wrappers to help you write your plugin in Rust (Zig wrapper in +/// development) +/// - A stubber for WASI +#[ty(scope)] #[derive(Clone)] pub struct Plugin(Arc<Repr>); @@ -36,6 +127,23 @@ struct StoreData { output: Vec<u8>, } +#[scope] +impl Plugin { + /// Creates a new plugin from a WebAssembly file. + #[func(constructor)] + pub fn construct( + /// The virtual machine. + vm: &mut Vm, + /// Path to a WebAssembly file. + path: Spanned<EcoString>, + ) -> SourceResult<Plugin> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + Plugin::new(data).at(span) + } +} + impl Plugin { /// Create a new plugin from raw WebAssembly bytes. #[comemo::memoize] @@ -179,10 +287,6 @@ impl Hash for Plugin { } } -cast! { - type Plugin: "plugin", -} - /// Write the arguments to the plugin function into the plugin's memory. fn wasm_minimal_protocol_write_args_to_buffer(mut caller: Caller<StoreData>, ptr: u32) { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); diff --git a/crates/typst/src/eval/scope.rs b/crates/typst/src/eval/scope.rs index b8d5b75a..35ff202a 100644 --- a/crates/typst/src/eval/scope.rs +++ b/crates/typst/src/eval/scope.rs @@ -1,11 +1,14 @@ -use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; +use std::hash::{Hash, Hasher}; use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; -use super::{IntoValue, Library, Value}; +use super::{ + Func, IntoValue, Library, Module, NativeFunc, NativeFuncData, NativeType, Type, Value, +}; use crate::diag::{bail, StrResult}; +use crate::model::{Element, NativeElement}; /// A stack of scopes. #[derive(Debug, Default, Clone)] @@ -83,18 +86,27 @@ fn unknown_variable(var: &str) -> EcoString { } /// A map from binding names to values. -#[derive(Default, Clone, Hash)] -pub struct Scope(BTreeMap<EcoString, Slot>, bool); +#[derive(Default, Clone)] +pub struct Scope { + map: IndexMap<EcoString, Slot>, + deduplicate: bool, + category: Option<&'static str>, +} impl Scope { /// Create a new empty scope. pub fn new() -> Self { - Self(BTreeMap::new(), false) + Default::default() } /// Create a new scope with duplication prevention. pub fn deduplicating() -> Self { - Self(BTreeMap::new(), true) + Self { deduplicate: true, ..Default::default() } + } + + /// Enter a new category. + pub fn category(&mut self, name: &'static str) { + self.category = Some(name); } /// Bind a value to a name. @@ -103,32 +115,68 @@ impl Scope { let name = name.into(); #[cfg(debug_assertions)] - if self.1 && self.0.contains_key(&name) { + if self.deduplicate && self.map.contains_key(&name) { panic!("duplicate definition: {name}"); } - self.0.insert(name, Slot::new(value.into_value(), Kind::Normal)); + self.map + .insert(name, Slot::new(value.into_value(), Kind::Normal, self.category)); + } + + /// Define a native function through a Rust type that shadows the function. + pub fn define_func<T: NativeFunc>(&mut self) { + let data = T::data(); + self.define(data.name, Func::from(data)); + } + + /// Define a native function with raw function data. + pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { + self.define(data.name, Func::from(data)); + } + + /// Define a native type. + pub fn define_type<T: NativeType>(&mut self) { + let data = T::data(); + self.define(data.name, Type::from(data)); + } + + /// Define a native element. + pub fn define_elem<T: NativeElement>(&mut self) { + let data = T::data(); + self.define(data.name, Element::from(data)); + } + + /// Define a module. + pub fn define_module(&mut self, module: Module) { + self.define(module.name().clone(), module); } /// Define a captured, immutable binding. pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) { - self.0 - .insert(var.into(), Slot::new(value.into_value(), Kind::Captured)); + self.map.insert( + var.into(), + Slot::new(value.into_value(), Kind::Captured, self.category), + ); } /// Try to access a variable immutably. pub fn get(&self, var: &str) -> Option<&Value> { - self.0.get(var).map(Slot::read) + self.map.get(var).map(Slot::read) } /// Try to access a variable mutably. pub fn get_mut(&mut self, var: &str) -> Option<StrResult<&mut Value>> { - self.0.get_mut(var).map(Slot::write) + self.map.get_mut(var).map(Slot::write) + } + + /// Get the category of a definition. + pub fn get_category(&self, var: &str) -> Option<&'static str> { + self.map.get(var)?.category } /// Iterate over all definitions. pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> { - self.0.iter().map(|(k, v)| (k, v.read())) + self.map.iter().map(|(k, v)| (k, v.read())) } } @@ -136,11 +184,22 @@ impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Scope ")?; f.debug_map() - .entries(self.0.iter().map(|(k, v)| (k, v.read()))) + .entries(self.map.iter().map(|(k, v)| (k, v.read()))) .finish() } } +impl Hash for Scope { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.map.len()); + for item in &self.map { + item.hash(state); + } + self.deduplicate.hash(state); + self.category.hash(state); + } +} + /// A slot where a value is stored. #[derive(Clone, Hash)] struct Slot { @@ -148,6 +207,8 @@ struct Slot { value: Value, /// The kind of slot, determines how the value can be accessed. kind: Kind, + /// The category of the slot. + category: Option<&'static str>, } /// The different kinds of slots. @@ -161,8 +222,8 @@ enum Kind { impl Slot { /// Create a new slot. - fn new(value: Value, kind: Kind) -> Self { - Self { value, kind } + fn new(value: Value, kind: Kind, category: Option<&'static str>) -> Self { + Self { value, kind, category } } /// Read the value. @@ -183,3 +244,12 @@ impl Slot { } } } + +/// Defines the associated scope of a Rust type. +pub trait NativeScope { + /// The constructor function for the type, if any. + fn constructor() -> Option<&'static NativeFuncData>; + + /// Get the associated scope for the type. + fn scope() -> Scope; +} diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs index 636b9640..6684389b 100644 --- a/crates/typst/src/eval/str.rs +++ b/crates/typst/src/eval/str.rs @@ -7,9 +7,14 @@ use ecow::EcoString; use serde::{Deserialize, Serialize}; use unicode_segmentation::UnicodeSegmentation; -use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; +use super::{ + cast, dict, func, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Type, Value, + Vm, +}; use crate::diag::{bail, At, SourceResult, StrResult}; -use crate::geom::GenAlign; +use crate::geom::Align; +use crate::model::Label; +use crate::syntax::{Span, Spanned}; /// Create a new [`Str`] from a format string. #[macro_export] @@ -22,10 +27,47 @@ macro_rules! __format_str { #[doc(inline)] pub use crate::__format_str as format_str; + #[doc(hidden)] pub use ecow::eco_format; -/// An immutable reference counted string. +/// A sequence of Unicode codepoints. +/// +/// You can iterate over the grapheme clusters of the string using a [for +/// loop]($scripting/#loops). Grapheme clusters are basically characters but +/// keep together things that belong together, e.g. multiple codepoints that +/// together form a flag emoji. Strings can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. +/// +/// Typst provides utility methods for string manipulation. Many of these +/// methods (e.g., `split`, `trim` and `replace`) operate on _patterns:_ A +/// pattern can be either a string or a [regular expression]($regex). This makes +/// the methods quite versatile. +/// +/// All lengths and indices are expressed in terms of UTF-8 bytes. Indices are +/// zero-based and negative indices wrap around to the end of the string. +/// +/// You can convert a value to a string with this type's constructor. +/// +/// # Example +/// ```example +/// #"hello world!" \ +/// #"\"hello\n world\"!" \ +/// #"1 2 3".split() \ +/// #"1,2;3".split(regex("[,;]")) \ +/// #(regex("\d+") in "ten euros") \ +/// #(regex("\d+") in "10 euros") +/// ``` +/// +/// # Escape sequences { #escapes } +/// Just like in markup, you can escape a few symbols in strings: +/// - `[\\]` for a backslash +/// - `[\"]` for a quote +/// - `[\n]` for a newline +/// - `[\r]` for a carriage return +/// - `[\t]` for a tab +/// - `[\u{1f600}]` for a hexadecimal Unicode escape sequence +#[ty(scope, title = "String")] #[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Str(EcoString); @@ -38,12 +80,15 @@ impl Str { /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.len() == 0 + self.0.is_empty() } - /// The length of the string in bytes. - pub fn len(&self) -> usize { - self.0.len() + /// Repeat the string a number of times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + if self.0.len().checked_mul(n).is_none() { + return Err(eco_format!("cannot repeat this string {n} times")); + } + Ok(Self(self.0.repeat(n))) } /// A string slice containing the entire string. @@ -51,8 +96,87 @@ impl Str { self } - /// Extract the first grapheme cluster. - pub fn first(&self) -> StrResult<Self> { + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult<usize> { + self.locate_opt(index)? + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds and on a valid char boundary. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + let resolved = wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()); + + if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) { + return Err(not_a_char_boundary(index)); + } + + Ok(resolved) + } +} + +#[scope] +impl Str { + /// Converts a value to a string. + /// + /// - Integers are formatted in base 10. This can be overridden with the + /// optional `base` parameter. + /// - Floats are formatted in base 10 and never in exponential notation. + /// - From labels the name is extracted. + /// - Bytes are decoded as UTF-8. + /// + /// If you wish to convert from and to Unicode code points, see the + /// [`to-unicode`]($str.to-unicode) and [`from-unicode`]($str.from-unicode) + /// functions. + /// + /// ```example + /// #str(10) \ + /// #str(4000, base: 16) \ + /// #str(2.7) \ + /// #str(1e8) \ + /// #str(<intro>) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a string. + value: ToStr, + /// The base (radix) to display integers in, between 2 and 36. + #[named] + #[default(Spanned::new(10, Span::detached()))] + base: Spanned<i64>, + ) -> SourceResult<Str> { + Ok(match value { + ToStr::Str(s) => { + if base.v != 10 { + bail!(base.span, "base is only supported for integers"); + } + s + } + ToStr::Int(n) => { + if base.v < 2 || base.v > 36 { + bail!(base.span, "base must be between 2 and 36"); + } + format_int_with_base(n, base.v).into() + } + }) + } + + /// The length of the string in UTF-8 encoded bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Extracts the first grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn first(&self) -> StrResult<Str> { self.0 .graphemes(true) .next() @@ -60,8 +184,10 @@ impl Str { .ok_or_else(string_is_empty) } - /// Extract the last grapheme cluster. - pub fn last(&self) -> StrResult<Self> { + /// Extracts the last grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn last(&self) -> StrResult<Str> { self.0 .graphemes(true) .next_back() @@ -69,8 +195,18 @@ impl Str { .ok_or_else(string_is_empty) } - /// Extract the grapheme cluster at the given index. - pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> { + /// Extracts the first grapheme cluster after the specified index. Returns + /// the default value if the index is out of bounds or fails with an error + /// if no default value was specified. + #[func] + pub fn at( + &self, + /// The byte index. If negative, indexes from the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { let len = self.len(); self.locate_opt(index)? .and_then(|i| self.0[i..].graphemes(true).next().map(|s| s.into_value())) @@ -78,41 +214,110 @@ impl Str { .ok_or_else(|| no_default_and_out_of_bounds(index, len)) } - /// Extract a contiguous substring. - pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + /// Extracts a substring of the string. + /// Fails with an error if the start or end index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start byte index (inclusive). If negative, indexes from the + /// back. + start: i64, + /// The end byte index (exclusive). If omitted, the whole slice until + /// the end of the string is extracted. If negative, indexes from the + /// back. + #[default] + end: Option<i64>, + /// The number of bytes to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Str> { + let end = end.or(count.map(|c| start + c)).unwrap_or(self.len() as i64); let start = self.locate(start)?; - let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + let end = self.locate(end)?.max(start); Ok(self.0[start..end].into()) } - /// The grapheme clusters the string consists of. + /// Returns the grapheme clusters of the string as an array of substrings. + #[func] pub fn clusters(&self) -> Array { self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() } - /// The codepoints the string consists of. + /// Returns the Unicode codepoints of the string as an array of substrings. + #[func] pub fn codepoints(&self) -> Array { self.chars().map(|c| Value::Str(c.into())).collect() } - /// Whether the given pattern exists in this string. - pub fn contains(&self, pattern: StrPattern) -> bool { + /// Converts a character into its corresponding code point. + /// + /// ```example + /// #"a".to-unicode() \ + /// #("a\u{0300}" + /// .codepoints() + /// .map(str.to-unicode)) + /// ``` + #[func] + pub fn to_unicode( + /// The character that should be converted. + character: char, + ) -> u32 { + character as u32 + } + + /// Converts a unicode code point into its corresponding string. + /// + /// ```example + /// #str.from-unicode(97) + /// ``` + #[func] + pub fn from_unicode( + /// The code point that should be converted. + value: u32, + ) -> StrResult<Str> { + let c: char = value + .try_into() + .map_err(|_| eco_format!("{value:#x} is not a valid codepoint"))?; + Ok(c.into()) + } + + /// Whether the string contains the specified pattern. + /// + /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` + /// instead of `{"abcd".contains("bc")}`. + #[func] + pub fn contains( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> bool { match pattern { StrPattern::Str(pat) => self.0.contains(pat.as_str()), StrPattern::Regex(re) => re.is_match(self), } } - /// Whether this string begins with the given pattern. - pub fn starts_with(&self, pattern: StrPattern) -> bool { + /// Whether the string starts with the specified pattern. + #[func] + pub fn starts_with( + &self, + /// The pattern the string might start with. + pattern: StrPattern, + ) -> bool { match pattern { StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), } } - /// Whether this string ends with the given pattern. - pub fn ends_with(&self, pattern: StrPattern) -> bool { + /// Whether the string ends with the specified pattern. + #[func] + pub fn ends_with( + &self, + /// The pattern the string might end with. + pattern: StrPattern, + ) -> bool { match pattern { StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), StrPattern::Regex(re) => { @@ -132,25 +337,52 @@ impl Str { } } - /// The text of the pattern's first match in this string. - pub fn find(&self, pattern: StrPattern) -> Option<Self> { + /// Searches for the specified pattern in the string and returns the first + /// match as a string or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<Str> { match pattern { StrPattern::Str(pat) => self.0.contains(pat.as_str()).then_some(pat), StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), } } - /// The position of the pattern's first match in this string. - pub fn position(&self, pattern: StrPattern) -> Option<i64> { + /// Searches for the specified pattern in the string and returns the index + /// of the first match as an integer or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<usize> { match pattern { - StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), - StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), + StrPattern::Str(pat) => self.0.find(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map(|m| m.start()), } } - /// The start and, text and capture groups (if any) of the first match of - /// the pattern in this string. - pub fn match_(&self, pattern: StrPattern) -> Option<Dict> { + /// Searches for the specified pattern in the string and returns a + /// dictionary with details about the first match or `{none}` if there is no + /// match. + /// + /// The returned dictionary has the following keys: + /// - `start`: The start offset of the match + /// - `end`: The end offset of the match + /// - `text`: The text that matched. + /// - `captures`: An array containing a string for each matched capturing + /// group. The first item of the array contains the first matched + /// capturing, not the whole match! This is empty unless the `pattern` was + /// a regex with capturing groups. + #[func] + pub fn match_( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<Dict> { match pattern { StrPattern::Str(pat) => { self.0.match_indices(pat.as_str()).next().map(match_to_dict) @@ -159,9 +391,15 @@ impl Str { } } - /// The start, end, text and capture groups (if any) of all matches of the - /// pattern in this string. - pub fn matches(&self, pattern: StrPattern) -> Array { + /// Searches for the specified pattern in the string and returns an array of + /// dictionaries with details about all matches. For details about the + /// returned dictionaries, see above. + #[func] + pub fn matches( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Array { match pattern { StrPattern::Str(pat) => self .0 @@ -177,30 +415,89 @@ impl Str { } } - /// Split this string at whitespace or a specific pattern. - pub fn split(&self, pattern: Option<StrPattern>) -> Array { - let s = self.as_str(); - match pattern { - None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), - Some(StrPattern::Str(pat)) => { - s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() + /// Replace at most `count` occurrences of the given pattern with a + /// replacement string or function (beginning from the start). If no count + /// is given, all occurrences are replaced. + #[func] + pub fn replace( + &self, + /// The virtual machine. + vm: &mut Vm, + /// The pattern to search for. + pattern: StrPattern, + /// The string to replace the matches with or a function that gets a + /// dictionary for each match and can return individual replacement + /// strings. + replacement: Replacement, + /// If given, only the first `count` matches of the pattern are placed. + #[named] + count: Option<usize>, + ) -> SourceResult<Str> { + // Heuristic: Assume the new string is about the same length as + // the current string. + let mut output = EcoString::with_capacity(self.as_str().len()); + + // Replace one match of a pattern with the replacement. + let mut last_match = 0; + let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> { + // Push everything until the match. + output.push_str(&self[last_match..range.start]); + last_match = range.end; + + // Determine and push the replacement. + match &replacement { + Replacement::Str(s) => output.push_str(s), + Replacement::Func(func) => { + let args = Args::new(func.span(), [dict.into_value()]); + let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?; + output.push_str(&piece); + } } - Some(StrPattern::Regex(re)) => { - re.split(s).map(|v| Value::Str(v.into())).collect() + + Ok(()) + }; + + // Iterate over the matches of the `pattern`. + let count = count.unwrap_or(usize::MAX); + match &pattern { + StrPattern::Str(pat) => { + for m in self.match_indices(pat.as_str()).take(count) { + let (start, text) = m; + handle_match(start..start + text.len(), match_to_dict(m))?; + } + } + StrPattern::Regex(re) => { + for caps in re.captures_iter(self).take(count) { + // Extract the entire match over all capture groups. + let m = caps.get(0).unwrap(); + handle_match(m.start()..m.end(), captures_to_dict(caps))?; + } } } + + // Push the remainder. + output.push_str(&self[last_match..]); + Ok(output.into()) } - /// Trim either whitespace or the given pattern at both or just one side of - /// the string. If `repeat` is true, the pattern is trimmed repeatedly - /// instead of just once. Repeat must only be given in combination with a - /// pattern. + /// Removes matches of a pattern from one or both sides of the string, once or + /// repeatedly and returns the resulting string. + #[func] pub fn trim( &self, + /// The pattern to search for. + #[default] pattern: Option<StrPattern>, + /// Can be `start` or `end` to only trim the start or end of the string. + /// If omitted, both sides are trimmed. + #[named] at: Option<StrSide>, + /// Whether to repeatedly removes matches of the pattern or just once. + /// Defaults to `{true}`. + #[named] + #[default(true)] repeat: bool, - ) -> Self { + ) -> Str { let mut start = matches!(at, Some(StrSide::Start) | None); let end = matches!(at, Some(StrSide::End) | None); @@ -268,101 +565,93 @@ impl Str { trimmed.into() } - /// Replace at most `count` occurrences of the given pattern with a - /// replacement string or function (beginning from the start). If no count - /// is given, all occurrences are replaced. - pub fn replace( + /// Splits a string at matches of a specified pattern and returns an array + /// of the resulting parts. + #[func] + pub fn split( &self, - vm: &mut Vm, - pattern: StrPattern, - with: Replacement, - count: Option<usize>, - ) -> SourceResult<Self> { - // Heuristic: Assume the new string is about the same length as - // the current string. - let mut output = EcoString::with_capacity(self.as_str().len()); - - // Replace one match of a pattern with the replacement. - let mut last_match = 0; - let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> { - // Push everything until the match. - output.push_str(&self[last_match..range.start]); - last_match = range.end; - - // Determine and push the replacement. - match &with { - Replacement::Str(s) => output.push_str(s), - Replacement::Func(func) => { - let args = Args::new(func.span(), [dict]); - let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?; - output.push_str(&piece); - } - } - - Ok(()) - }; - - // Iterate over the matches of the `pattern`. - let count = count.unwrap_or(usize::MAX); - match &pattern { - StrPattern::Str(pat) => { - for m in self.match_indices(pat.as_str()).take(count) { - let (start, text) = m; - handle_match(start..start + text.len(), match_to_dict(m))?; - } + /// The pattern to split at. Defaults to whitespace. + #[default] + pattern: Option<StrPattern>, + ) -> Array { + let s = self.as_str(); + match pattern { + None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), + Some(StrPattern::Str(pat)) => { + s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() } - StrPattern::Regex(re) => { - for caps in re.captures_iter(self).take(count) { - // Extract the entire match over all capture groups. - let m = caps.get(0).unwrap(); - handle_match(m.start()..m.end(), captures_to_dict(caps))?; - } + Some(StrPattern::Regex(re)) => { + re.split(s).map(|v| Value::Str(v.into())).collect() } } + } - // Push the remainder. - output.push_str(&self[last_match..]); - Ok(output.into()) + /// Reverse the string. + #[func(title = "Reverse")] + pub fn rev(&self) -> Str { + self.as_str().graphemes(true).rev().collect::<String>().into() } +} - /// Repeat the string a number of times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let n = usize::try_from(n) - .ok() - .and_then(|n| self.0.len().checked_mul(n).map(|_| n)) - .ok_or_else(|| format!("cannot repeat this string {} times", n))?; +/// A value that can be cast to a string. +pub enum ToStr { + /// A string value ready to be used as-is. + Str(Str), + /// An integer about to be formatted in a given base. + Int(i64), +} - Ok(Self(self.0.repeat(n))) - } +cast! { + ToStr, + v: i64 => Self::Int(v), + v: f64 => Self::Str(format_str!("{}", v)), + v: Bytes => Self::Str( + std::str::from_utf8(&v) + .map_err(|_| "bytes are not valid utf-8")? + .into() + ), + v: Label => Self::Str(v.0.into()), + v: Type => Self::Str(v.long_name().into()), + v: Str => Self::Str(v), +} - /// Reverse the string. - pub fn rev(&self) -> Self { - self.as_str().graphemes(true).rev().collect::<String>().into() +/// Format an integer in a base. +fn format_int_with_base(mut n: i64, base: i64) -> EcoString { + if n == 0 { + return "0".into(); } - /// Resolve an index or throw an out of bounds error. - fn locate(&self, index: i64) -> StrResult<usize> { - self.locate_opt(index)? - .ok_or_else(|| out_of_bounds(index, self.len())) + // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`. + // So we can only use the built-in for decimal, not bin/oct/hex. + if base == 10 { + return eco_format!("{n}"); } - /// Resolve an index, if it is within bounds and on a valid char boundary. - /// - /// `index == len` is considered in bounds. - fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { - let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long. + const SIZE: usize = 65; + let mut digits = [b'\0'; SIZE]; + let mut i = SIZE; - let resolved = wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()); + // It's tempting to take the absolute value, but this will fail for i64::MIN. + // Instead, we turn n negative, as -i64::MAX is perfectly representable. + let negative = n < 0; + if n > 0 { + n = -n; + } - if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) { - return Err(not_a_char_boundary(index)); - } + while n != 0 { + let digit = char::from_digit(-(n % base) as u32, base as u32); + i -= 1; + digits[i] = digit.unwrap_or('?') as u8; + n /= base; + } - Ok(resolved) + if negative { + i -= 1; + digits[i] = b'-'; } + + std::str::from_utf8(&digits[i..]).unwrap_or_default().into() } /// The out of bounds access error message. @@ -547,6 +836,25 @@ cast! { } /// A regular expression. +/// +/// Can be used as a [show rule selector]($styling/#show-rules) and with +/// [string methods]($str) like `find`, `split`, and `replace`. +/// +/// [See here](https://docs.rs/regex/latest/regex/#syntax) for a specification +/// of the supported syntax. +/// +/// # Example +/// ```example +/// // Works with show rules. +/// #show regex("\d+"): set text(red) +/// +/// The numbers 1 to 10. +/// +/// // Works with string methods. +/// #("a,b;c" +/// .split(regex("[,;]"))) +/// ``` +#[ty(scope)] #[derive(Clone)] pub struct Regex(regex::Regex); @@ -557,6 +865,27 @@ impl Regex { } } +#[scope] +impl Regex { + /// Create a regular expression from a string. + #[func(constructor)] + pub fn construct( + /// The regular expression as a string. + /// + /// Most regex escape sequences just work because they are not valid Typst + /// escape sequences. To produce regex escape sequences that are also valid in + /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim + /// backslash, you would need to write `{regex("\\\\")}`. + /// + /// If you need many escape sequences, you can also create a raw element + /// and extract its text to use it for your regular expressions: + /// ```{regex(`\d+\.\d+\.\d+`.text)}```. + regex: Spanned<Str>, + ) -> SourceResult<Regex> { + Self::new(®ex.v).at(regex.span) + } +} + impl Deref for Regex { type Target = regex::Regex; @@ -584,7 +913,7 @@ impl Hash for Regex { } cast! { - type Regex: "regular expression", + type Regex, } /// A pattern which can be searched for in a string. @@ -598,8 +927,12 @@ pub enum StrPattern { cast! { StrPattern, - text: Str => Self::Str(text), - regex: Regex => Self::Regex(regex), + self => match self { + Self::Str(v) => v.into_value(), + Self::Regex(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Regex => Self::Regex(v), } /// A side of a string. @@ -614,9 +947,9 @@ pub enum StrSide { cast! { StrSide, - align: GenAlign => match align { - GenAlign::Start => Self::Start, - GenAlign::End => Self::End, + v: Align => match v { + Align::START => Self::Start, + Align::END => Self::End, _ => bail!("expected either `start` or `end`"), }, } @@ -632,6 +965,36 @@ pub enum Replacement { cast! { Replacement, - text: Str => Self::Str(text), - func: Func => Self::Func(func) + self => match self { + Self::Str(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Func => Self::Func(v) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_base() { + assert_eq!(&format_int_with_base(0, 10), "0"); + assert_eq!(&format_int_with_base(0, 16), "0"); + assert_eq!(&format_int_with_base(0, 36), "0"); + assert_eq!( + &format_int_with_base(i64::MAX, 2), + "111111111111111111111111111111111111111111111111111111111111111" + ); + assert_eq!( + &format_int_with_base(i64::MIN, 2), + "-1000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!(&format_int_with_base(i64::MAX, 10), "9223372036854775807"); + assert_eq!(&format_int_with_base(i64::MIN, 10), "-9223372036854775808"); + assert_eq!(&format_int_with_base(i64::MAX, 16), "7fffffffffffffff"); + assert_eq!(&format_int_with_base(i64::MIN, 16), "-8000000000000000"); + assert_eq!(&format_int_with_base(i64::MAX, 36), "1y2p0ij32e8e7"); + assert_eq!(&format_int_with_base(i64::MIN, 36), "-1y2p0ij32e8e8"); + } } diff --git a/crates/typst/src/eval/symbol.rs b/crates/typst/src/eval/symbol.rs index 58cfd534..ec5da51c 100644 --- a/crates/typst/src/eval/symbol.rs +++ b/crates/typst/src/eval/symbol.rs @@ -6,9 +6,43 @@ use std::sync::Arc; use ecow::EcoString; use serde::{Serialize, Serializer}; -use crate::diag::{bail, StrResult}; +use super::{cast, func, scope, ty, Array}; +use crate::diag::{bail, SourceResult, StrResult}; +use crate::syntax::{Span, Spanned}; -/// A symbol, possibly with variants. +#[doc(inline)] +pub use typst_macros::symbols; + +/// A Unicode symbol. +/// +/// Typst defines common symbols so that they can easily be written with +/// standard keyboards. The symbols are defined in modules, from which they can +/// be accessed using [field access notation]($scripting/#fields): +/// +/// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) +/// +/// Moreover, you can define custom symbols with this type's constructor +/// function. +/// +/// ```example +/// #sym.arrow.r \ +/// #sym.gt.eq.not \ +/// $gt.eq.not$ \ +/// #emoji.face.halo +/// ``` +/// +/// Many symbols have different variants, which can be selected by appending the +/// modifiers with dot notation. The order of the modifiers is not relevant. +/// Visit the documentation pages of the symbol modules and click on a symbol to +/// see its available variants. +/// +/// ```example +/// $arrow.l$ \ +/// $arrow.r$ \ +/// $arrow.t.quad$ +/// ``` +#[ty(scope)] #[derive(Clone, Eq, PartialEq, Hash)] pub struct Symbol(Repr); @@ -29,7 +63,7 @@ enum List { impl Symbol { /// Create a new symbol from a single character. - pub const fn new(c: char) -> Self { + pub const fn single(c: char) -> Self { Self(Repr::Single(c)) } @@ -124,6 +158,53 @@ impl Symbol { } } +#[scope] +impl Symbol { + /// Create a custom symbol with modifiers. + /// + /// ```example + /// #let envelope = symbol( + /// "🖂", + /// ("stamped", "🖃"), + /// ("stamped.pen", "🖆"), + /// ("lightning", "🖄"), + /// ("fly", "🖅"), + /// ) + /// + /// #envelope + /// #envelope.stamped + /// #envelope.stamped.pen + /// #envelope.lightning + /// #envelope.fly + /// ``` + #[func(constructor)] + pub fn construct( + /// The callsite span. + span: Span, + /// The variants of the symbol. + /// + /// Can be a just a string consisting of a single character for the + /// modifierless variant or an array with two strings specifying the modifiers + /// and the symbol. Individual modifiers should be separated by dots. When + /// displaying a symbol, Typst selects the first from the variants that have + /// all attached modifiers and the minimum number of other modifiers. + #[variadic] + variants: Vec<Spanned<Variant>>, + ) -> SourceResult<Symbol> { + let mut list = Vec::new(); + if variants.is_empty() { + bail!(span, "expected at least one variant"); + } + for Spanned { v, span } in variants { + if list.iter().any(|(prev, _)| &v.0 == prev) { + bail!(span, "duplicate variant"); + } + list.push((v.0, v.1)); + } + Ok(Symbol::runtime(list.into_boxed_slice())) + } +} + impl Debug for Symbol { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_char(self.get()) @@ -155,6 +236,21 @@ impl List { } } +/// A value that can be cast to a symbol. +pub struct Variant(EcoString, char); + +cast! { + Variant, + c: char => Self(EcoString::new(), c), + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + /// Iterator over variants. enum Variants<'a> { Single(std::option::IntoIter<char>), diff --git a/crates/typst/src/eval/ty.rs b/crates/typst/src/eval/ty.rs new file mode 100644 index 00000000..f7832672 --- /dev/null +++ b/crates/typst/src/eval/ty.rs @@ -0,0 +1,165 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter}; + +use ecow::eco_format; +use once_cell::sync::Lazy; + +use super::{cast, func, Func, NativeFuncData, Scope, Value}; +use crate::diag::StrResult; +use crate::util::Static; + +#[doc(inline)] +pub use typst_macros::{scope, ty}; + +/// Describes a kind of value. +/// +/// To style your document, you need to work with values of different kinds: +/// Lengths specifying the size of your elements, colors for your text and +/// shapes, and more. Typst categorizes these into clearly defined _types_ and +/// tells you where it expects which type of value. +/// +/// Apart from very basic types for numeric values and [typical]($int) +/// [types]($float) [known]($str) [from]($array) [programming]($dictionary) +/// languages, Typst provides a special type for [_content._]($content) A value +/// of this type can hold anything that you can enter into your document: Text, +/// elements like headings and shapes, and style information. +#[ty(scope)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Type(Static<NativeTypeData>); + +impl Type { + /// Get the type for `T`. + pub fn of<T: NativeType>() -> Self { + T::ty() + } + + /// The type's short name, how it is used in code (e.g. `str`). + pub fn short_name(&self) -> &'static str { + self.0.name + } + + /// The type's long name, for use in diagnostics (e.g. `string`). + pub fn long_name(&self) -> &'static str { + self.0.long_name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the type (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs + } + + /// Search keywords for the type. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// This type's constructor function. + pub fn constructor(&self) -> StrResult<Func> { + self.0 + .constructor + .as_ref() + .map(|lazy| Func::from(*lazy)) + .ok_or_else(|| eco_format!("type {self} does not have a constructor")) + } + + /// The type's associated scope of sub-definition. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Get a field from this type's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + self.scope() + .get(field) + .ok_or_else(|| eco_format!("type {self} does not contain field `{}`", field)) + } +} + +#[scope] +impl Type { + /// Determines a value's type. + /// + /// ```example + /// #type(12) \ + /// #type(14.7) \ + /// #type("hello") \ + /// #type(<glacier>) \ + /// #type([Hi]) \ + /// #type(x => x + 1) \ + /// #type(type) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value whose type's to determine. + value: Value, + ) -> Type { + value.ty() + } +} + +impl Debug for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.long_name()) + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.long_name()) + } +} + +impl Ord for Type { + fn cmp(&self, other: &Self) -> Ordering { + self.long_name().cmp(other.long_name()) + } +} + +impl PartialOrd for Type { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +/// A Typst type that is defined by a native Rust type. +pub trait NativeType { + /// The type's name. + /// + /// In contrast to `data()`, this is usable in const contexts. + const NAME: &'static str; + + /// Get the type for the native Rust type. + fn ty() -> Type { + Type::from(Self::data()) + } + + // Get the type data for the native Rust type. + fn data() -> &'static NativeTypeData; +} + +/// Defines a native type. +pub struct NativeTypeData { + pub name: &'static str, + pub long_name: &'static str, + pub title: &'static str, + pub docs: &'static str, + pub keywords: &'static [&'static str], + pub constructor: Lazy<Option<&'static NativeFuncData>>, + pub scope: Lazy<Scope>, +} + +impl From<&'static NativeTypeData> for Type { + fn from(data: &'static NativeTypeData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeTypeData, + self => Type::from(self).into_value(), +} diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index 4e870ab4..539cb516 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/eval/value.rs @@ -9,12 +9,12 @@ use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer}; use serde::de::{Error, MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use siphasher::sip128::{Hasher128, SipHasher13}; -use time::macros::format_description; use typst::eval::Duration; use super::{ - cast, fields, format_str, ops, Args, Array, Bytes, CastInfo, Content, Dict, - FromValue, Func, IntoValue, Module, Reflect, Str, Symbol, + fields, format_str, ops, Args, Array, AutoValue, Bytes, CastInfo, Content, Dict, + FromValue, Func, IntoValue, Module, NativeType, NoneValue, Plugin, Reflect, Scope, + Str, Symbol, Type, }; use crate::diag::StrResult; use crate::eval::Datetime; @@ -72,8 +72,12 @@ pub enum Value { Func(Func), /// Captured arguments to a function. Args(Args), + /// A type. + Type(Type), /// A module. Module(Module), + /// A WebAssembly plugin. + Plugin(Plugin), /// A dynamic value. Dyn(Dynamic), } @@ -82,7 +86,7 @@ impl Value { /// Create a new dynamic value. pub fn dynamic<T>(any: T) -> Self where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static, { Self::Dyn(Dynamic::new(any)) } @@ -103,34 +107,36 @@ impl Value { } } - /// The name of the stored value's type. - pub fn type_name(&self) -> &'static str { + /// The type of this value. + pub fn ty(&self) -> Type { match self { - Self::None => "none", - Self::Auto => "auto", - Self::Bool(_) => bool::TYPE_NAME, - Self::Int(_) => i64::TYPE_NAME, - Self::Float(_) => f64::TYPE_NAME, - Self::Length(_) => Length::TYPE_NAME, - Self::Angle(_) => Angle::TYPE_NAME, - Self::Ratio(_) => Ratio::TYPE_NAME, - Self::Relative(_) => Rel::<Length>::TYPE_NAME, - Self::Fraction(_) => Fr::TYPE_NAME, - Self::Color(_) => Color::TYPE_NAME, - Self::Symbol(_) => Symbol::TYPE_NAME, - Self::Str(_) => Str::TYPE_NAME, - Self::Bytes(_) => Bytes::TYPE_NAME, - Self::Label(_) => Label::TYPE_NAME, - Self::Datetime(_) => Datetime::TYPE_NAME, - Self::Duration(_) => Duration::TYPE_NAME, - Self::Content(_) => Content::TYPE_NAME, - Self::Styles(_) => Styles::TYPE_NAME, - Self::Array(_) => Array::TYPE_NAME, - Self::Dict(_) => Dict::TYPE_NAME, - Self::Func(_) => Func::TYPE_NAME, - Self::Args(_) => Args::TYPE_NAME, - Self::Module(_) => Module::TYPE_NAME, - Self::Dyn(v) => v.type_name(), + Self::None => Type::of::<NoneValue>(), + Self::Auto => Type::of::<AutoValue>(), + Self::Bool(_) => Type::of::<bool>(), + Self::Int(_) => Type::of::<i64>(), + Self::Float(_) => Type::of::<f64>(), + Self::Length(_) => Type::of::<Length>(), + Self::Angle(_) => Type::of::<Angle>(), + Self::Ratio(_) => Type::of::<Ratio>(), + Self::Relative(_) => Type::of::<Rel<Length>>(), + Self::Fraction(_) => Type::of::<Fr>(), + Self::Color(_) => Type::of::<Color>(), + Self::Symbol(_) => Type::of::<Symbol>(), + Self::Str(_) => Type::of::<Str>(), + Self::Bytes(_) => Type::of::<Bytes>(), + Self::Label(_) => Type::of::<Label>(), + Self::Datetime(_) => Type::of::<Datetime>(), + Self::Duration(_) => Type::of::<Duration>(), + Self::Content(_) => Type::of::<Content>(), + Self::Styles(_) => Type::of::<Styles>(), + Self::Array(_) => Type::of::<Array>(), + Self::Dict(_) => Type::of::<Dict>(), + Self::Func(_) => Type::of::<Func>(), + Self::Args(_) => Type::of::<Args>(), + Self::Type(_) => Type::of::<Type>(), + Self::Module(_) => Type::of::<Module>(), + Self::Plugin(_) => Type::of::<Module>(), + Self::Dyn(v) => v.ty(), } } @@ -143,48 +149,59 @@ impl Value { pub fn field(&self, field: &str) -> StrResult<Value> { match self { Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), - Self::Dict(dict) => dict.at(field, None), - Self::Content(content) => content.at(field, None), - Self::Module(module) => module.get(field).cloned(), - Self::Func(func) => func.get(field).cloned(), + Self::Dict(dict) => dict.get(field).cloned(), + Self::Content(content) => content.get(field), + Self::Type(ty) => ty.field(field).cloned(), + Self::Func(func) => func.field(field).cloned(), + Self::Module(module) => module.field(field).cloned(), _ => fields::field(self, field), } } - /// Return the debug representation of the value. - pub fn repr(&self) -> Str { - format_str!("{:?}", self) + /// The associated scope, if this is a function, type, or module. + pub fn scope(&self) -> Option<&Scope> { + match self { + Self::Func(func) => func.scope(), + Self::Type(ty) => Some(ty.scope()), + Self::Module(module) => Some(module.scope()), + _ => None, + } } - /// Attach a span to the value, if possible. - pub fn spanned(self, span: Span) -> Self { + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { match self { - Value::Content(v) => Value::Content(v.spanned(span)), - Value::Func(v) => Value::Func(v.spanned(span)), - v => v, + Self::Func(func) => func.docs(), + Self::Type(ty) => Some(ty.docs()), + _ => None, } } + /// Return the debug representation of the value. + pub fn repr(&self) -> Str { + format_str!("{self:?}") + } + /// Return the display representation of the value. pub fn display(self) -> Content { match self { Self::None => Content::empty(), - Self::Int(v) => item!(text)(eco_format!("{}", v)), - Self::Float(v) => item!(text)(eco_format!("{}", v)), + Self::Int(v) => item!(text)(eco_format!("{v}")), + Self::Float(v) => item!(text)(eco_format!("{v}")), Self::Str(v) => item!(text)(v.into()), Self::Symbol(v) => item!(text)(v.get().into()), Self::Content(v) => v, - Self::Func(_) => Content::empty(), Self::Module(module) => module.content(), _ => item!(raw)(self.repr().into(), Some("typc".into()), false), } } - /// Try to extract documentation for the value. - pub fn docs(&self) -> Option<&'static str> { + /// Attach a span to the value, if possible. + pub fn spanned(self, span: Span) -> Self { match self { - Self::Func(func) => func.info().map(|info| info.docs), - _ => None, + Value::Content(v) => Value::Content(v.spanned(span)), + Value::Func(v) => Value::Func(v.spanned(span)), + v => v, } } } @@ -192,8 +209,8 @@ impl Value { impl Debug for Value { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::None => f.pad("none"), - Self::Auto => f.pad("auto"), + Self::None => Debug::fmt(&NoneValue, f), + Self::Auto => Debug::fmt(&AutoValue, f), Self::Bool(v) => Debug::fmt(v, f), Self::Int(v) => Debug::fmt(v, f), Self::Float(v) => Debug::fmt(v, f), @@ -215,7 +232,9 @@ impl Debug for Value { Self::Dict(v) => Debug::fmt(v, f), Self::Func(v) => Debug::fmt(v, f), Self::Args(v) => Debug::fmt(v, f), + Self::Type(v) => Debug::fmt(v, f), Self::Module(v) => Debug::fmt(v, f), + Self::Plugin(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } } @@ -260,7 +279,9 @@ impl Hash for Value { Self::Dict(v) => v.hash(state), Self::Func(v) => v.hash(state), Self::Args(v) => v.hash(state), + Self::Type(v) => v.hash(state), Self::Module(v) => v.hash(state), + Self::Plugin(v) => v.hash(state), Self::Dyn(v) => v.hash(state), } } @@ -272,10 +293,10 @@ impl Serialize for Value { S: Serializer, { match self { - Self::None => serializer.serialize_none(), - Self::Bool(v) => serializer.serialize_bool(*v), - Self::Int(v) => serializer.serialize_i64(*v), - Self::Float(v) => serializer.serialize_f64(*v), + Self::None => NoneValue.serialize(serializer), + Self::Bool(v) => v.serialize(serializer), + Self::Int(v) => v.serialize(serializer), + Self::Float(v) => v.serialize(serializer), Self::Str(v) => v.serialize(serializer), Self::Bytes(v) => v.serialize(serializer), Self::Symbol(v) => v.serialize(serializer), @@ -289,59 +310,16 @@ impl Serialize for Value { } } -fn parse_toml_date(dict: &Dict) -> Option<Datetime> { - if dict.len() != 1 || !dict.contains("$__toml_private_datetime") { - return None; - } - - let Ok(s) = String::from_value(dict.at("$__toml_private_datetime", None).unwrap()) - else { - return None; - }; - - if let Ok(d) = time::PrimitiveDateTime::parse( - &s, - &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), - ) { - Some( - Datetime::from_ymd_hms( - d.year(), - d.month() as u8, - d.day(), - d.hour(), - d.minute(), - d.second(), - ) - .unwrap(), - ) - } else if let Ok(d) = time::PrimitiveDateTime::parse( - &s, - &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), - ) { - Some( - Datetime::from_ymd_hms( - d.year(), - d.month() as u8, - d.day(), - d.hour(), - d.minute(), - d.second(), - ) - .unwrap(), - ) - } else if let Ok(d) = - time::Date::parse(&s, &format_description!("[year]-[month]-[day]")) - { - Some(Datetime::from_ymd(d.year(), d.month() as u8, d.day()).unwrap()) - } else if let Ok(d) = - time::Time::parse(&s, &format_description!("[hour]:[minute]:[second]")) +impl<'de> Deserialize<'de> for Value { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, { - Some(Datetime::from_hms(d.hour(), d.minute(), d.second()).unwrap()) - } else { - None + deserializer.deserialize_any(ValueVisitor) } } +/// Visitor for value deserialization. struct ValueVisitor; impl<'de> Visitor<'de> for ValueVisitor { @@ -444,23 +422,14 @@ impl<'de> Visitor<'de> for ValueVisitor { fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> { let dict = Dict::deserialize(MapAccessDeserializer::new(map))?; - Ok(match parse_toml_date(&dict) { + Ok(match Datetime::from_toml_dict(&dict) { None => dict.into_value(), Some(datetime) => datetime.into_value(), }) } } -impl<'de> Deserialize<'de> for Value { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(ValueVisitor) - } -} - -/// A dynamic value. +/// A value that is not part of the built-in enum. #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] pub struct Dynamic(Arc<dyn Bounds>); @@ -469,24 +438,24 @@ impl Dynamic { /// Create a new instance from any value that satisfies the required bounds. pub fn new<T>(any: T) -> Self where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static, { Self(Arc::new(any)) } /// Whether the wrapped type is `T`. - pub fn is<T: Type + 'static>(&self) -> bool { + pub fn is<T: 'static>(&self) -> bool { (*self.0).as_any().is::<T>() } /// Try to downcast to a reference to a specific type. - pub fn downcast<T: Type + 'static>(&self) -> Option<&T> { + pub fn downcast<T: 'static>(&self) -> Option<&T> { (*self.0).as_any().downcast_ref() } /// The name of the stored value's type. - pub fn type_name(&self) -> &'static str { - self.0.dyn_type_name() + pub fn ty(&self) -> Type { + self.0.dyn_ty() } } @@ -502,21 +471,16 @@ impl PartialEq for Dynamic { } } -cast! { - Dynamic, - self => Value::Dyn(self), -} - trait Bounds: Debug + Sync + Send + 'static { fn as_any(&self) -> &dyn Any; fn dyn_eq(&self, other: &Dynamic) -> bool; - fn dyn_type_name(&self) -> &'static str; + fn dyn_ty(&self) -> Type; fn hash128(&self) -> u128; } impl<T> Bounds for T where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static, { fn as_any(&self) -> &dyn Any { self @@ -527,8 +491,8 @@ where self == other } - fn dyn_type_name(&self) -> &'static str { - T::TYPE_NAME + fn dyn_ty(&self) -> Type { + Type::of::<T>() } #[tracing::instrument(skip_all)] @@ -548,25 +512,19 @@ impl Hash for dyn Bounds { } } -/// The type of a value. -pub trait Type { - /// The name of the type. - const TYPE_NAME: &'static str; -} - -/// Implement traits for primitives. +/// Implements traits for primitives (Value enum variants). macro_rules! primitive { ( $ty:ty: $name:literal, $variant:ident $(, $other:ident$(($binding:ident))? => $out:expr)* ) => { - impl Type for $ty { - const TYPE_NAME: &'static str = $name; - } - impl Reflect for $ty { - fn describe() -> CastInfo { - CastInfo::Type(Self::TYPE_NAME) + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) } fn castable(value: &Value) -> bool { @@ -588,8 +546,8 @@ macro_rules! primitive { $(Value::$other$(($binding))? => Ok($out),)* v => Err(eco_format!( "expected {}, found {}", - Self::TYPE_NAME, - v.type_name(), + Type::of::<Self>(), + v.ty(), )), } } @@ -632,9 +590,15 @@ primitive! { Content: "content", primitive! { Styles: "styles", Styles } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } -primitive! { Func: "function", Func } +primitive! { + Func: "function", + Func, + Type(ty) => ty.constructor()?.clone() +} primitive! { Args: "arguments", Args } +primitive! { Type: "type", Type } primitive! { Module: "module", Module } +primitive! { Plugin: "plugin", Plugin } #[cfg(test)] mod tests { diff --git a/crates/typst/src/export/pdf/external_graphics_state.rs b/crates/typst/src/export/pdf/extg.rs index 164de1b6..f62bec6a 100644 --- a/crates/typst/src/export/pdf/external_graphics_state.rs +++ b/crates/typst/src/export/pdf/extg.rs @@ -1,6 +1,7 @@ -use crate::export::pdf::{PdfContext, RefExt}; use pdf_writer::Finish; +use crate::export::pdf::{PdfContext, RefExt}; + /// A PDF external graphics state. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct ExternalGraphicsState { diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs index b650d5b9..3e3f2650 100644 --- a/crates/typst/src/export/pdf/mod.rs +++ b/crates/typst/src/export/pdf/mod.rs @@ -1,6 +1,6 @@ //! Exporting into PDF documents. -mod external_graphics_state; +mod extg; mod font; mod image; mod outline; @@ -22,7 +22,7 @@ use crate::geom::{Abs, Dir, Em}; use crate::image::Image; use crate::model::Introspector; -use external_graphics_state::ExternalGraphicsState; +use extg::ExternalGraphicsState; /// Export a document into a PDF file. /// @@ -33,7 +33,7 @@ pub fn pdf(document: &Document) -> Vec<u8> { page::construct_pages(&mut ctx, &document.pages); font::write_fonts(&mut ctx); image::write_images(&mut ctx); - external_graphics_state::write_external_graphics_states(&mut ctx); + extg::write_external_graphics_states(&mut ctx); page::write_page_tree(&mut ctx); write_catalog(&mut ctx); ctx.writer.finish() diff --git a/crates/typst/src/export/pdf/outline.rs b/crates/typst/src/export/pdf/outline.rs index 89d01135..855d9f4f 100644 --- a/crates/typst/src/export/pdf/outline.rs +++ b/crates/typst/src/export/pdf/outline.rs @@ -17,7 +17,7 @@ pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> { // Therefore, its next descendant must be added at its level, which is // enforced in the manner shown below. let mut last_skipped_level = None; - for heading in ctx.introspector.query(&item!(heading_func).select()) { + for heading in ctx.introspector.query(&item!(heading_elem).select()) { let leaf = HeadingNode::leaf((*heading).clone()); if leaf.bookmarked { diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs index 0c0d2957..c3d389ea 100644 --- a/crates/typst/src/export/pdf/page.rs +++ b/crates/typst/src/export/pdf/page.rs @@ -5,13 +5,13 @@ use pdf_writer::types::{ use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; -use super::external_graphics_state::ExternalGraphicsState; +use super::extg::ExternalGraphicsState; use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::font::Font; use crate::geom::{ - self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio, - Shape, Size, Stroke, Transform, + self, Abs, Color, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, + Point, Ratio, Shape, Size, Transform, }; use crate::image::Image; @@ -209,7 +209,7 @@ struct State { fill: Option<Paint>, fill_space: Option<Name<'static>>, external_graphics_state: Option<ExternalGraphicsState>, - stroke: Option<Stroke>, + stroke: Option<FixedStroke>, stroke_space: Option<Name<'static>>, } @@ -237,7 +237,7 @@ impl PageContext<'_, '_> { } } - fn set_opacities(&mut self, stroke: Option<&Stroke>, fill: Option<&Paint>) { + fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { let stroke_opacity = stroke .map(|stroke| { let Paint::Solid(color) = stroke.paint; @@ -319,9 +319,9 @@ impl PageContext<'_, '_> { self.state.fill_space = None; } - fn set_stroke(&mut self, stroke: &Stroke) { + fn set_stroke(&mut self, stroke: &FixedStroke) { if self.state.stroke.as_ref() != Some(stroke) { - let Stroke { + let FixedStroke { paint, thickness, line_cap, diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs index 145e64a3..10a3e813 100644 --- a/crates/typst/src/export/render.rs +++ b/crates/typst/src/export/render.rs @@ -14,8 +14,8 @@ use usvg::{NodeExt, TreeParsing}; use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::font::Font; use crate::geom::{ - self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke, - Transform, + self, Abs, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, + Size, Transform, }; use crate::image::{DecodedImage, Image}; @@ -466,7 +466,7 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - if let Some(Stroke { + if let Some(FixedStroke { paint, thickness, line_cap, diff --git a/crates/typst/src/export/svg.rs b/crates/typst/src/export/svg.rs index 24c4b1a3..7d67274c 100644 --- a/crates/typst/src/export/svg.rs +++ b/crates/typst/src/export/svg.rs @@ -10,8 +10,8 @@ use xmlwriter::XmlWriter; use crate::doc::{Frame, FrameItem, GroupItem, TextItem}; use crate::font::Font; use crate::geom::{ - Abs, Axes, Geometry, LineCap, LineJoin, Paint, PathItem, Ratio, Shape, Size, Stroke, - Transform, + Abs, Axes, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Ratio, Shape, + Size, Transform, }; use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use crate::util::hash128; @@ -303,7 +303,7 @@ impl SVGRenderer { } /// Write a stroke attribute. - fn write_stroke(&mut self, stroke: &Stroke) { + fn write_stroke(&mut self, stroke: &FixedStroke) { let Paint::Solid(color) = stroke.paint; self.xml.write_attribute("stroke", &color.to_rgba().to_hex()); self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt()); diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/geom/align.rs index 2007db96..bfe377fb 100644 --- a/crates/typst/src/geom/align.rs +++ b/crates/typst/src/geom/align.rs @@ -1,248 +1,379 @@ use super::*; -/// Where to align something along an axis. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +/// Where to [align]($align) something along an axis. +/// +/// Possible values are: +/// - `start`: Aligns at the [start]($direction.start) of the [text +/// direction]($text.dir). +/// - `end`: Aligns at the [end]($direction.end) of the [text +/// direction]($text.dir). +/// - `left`: Align at the left. +/// - `center`: Aligns in the middle, horizontally. +/// - `right`: Aligns at the right. +/// - `top`: Aligns at the top. +/// - `horizon`: Aligns in the middle, vertically. +/// - `bottom`: Align at the bottom. +/// +/// These values are available globally and also in the alignment type's scope, +/// so you can write either of the following two: +/// +/// ```example +/// #align(center)[Hi] +/// #align(alignment.center)[Hi] +/// ``` +/// +/// # 2D alignments +/// To align along both axes at the same time, add the two alignments using the +/// `+` operator. For example, `top + right` aligns the content to the top right +/// corner. +/// +/// ```example +/// #set page(height: 3cm) +/// #align(center + bottom)[Hi] +/// ``` +/// +/// # Fields +/// The `x` and `y` fields hold the alignment's horizontal and vertical +/// components, respectively (as yet another `alignment`). They may be `{none}`. +/// +/// ```example +/// #(top + right).x \ +/// #left.x \ +/// #left.y (none) +/// ``` +#[ty(scope, name = "alignment")] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Align { - /// Align at the left side. - Left, - /// Align in the horizontal middle. - Center, - /// Align at the right side. - Right, - /// Align at the top side. - Top, - /// Align in the vertical middle. - Horizon, - /// Align at the bottom side. - Bottom, + H(HAlign), + V(VAlign), + Both(HAlign, VAlign), } impl Align { - /// Top-left alignment. - pub const LEFT_TOP: Axes<Self> = Axes { x: Align::Left, y: Align::Top }; + /// The horizontal component. + pub const fn x(self) -> Option<HAlign> { + match self { + Self::H(x) | Self::Both(x, _) => Some(x), + Self::V(_) => None, + } + } + + /// The vertical component. + pub const fn y(self) -> Option<VAlign> { + match self { + Self::V(y) | Self::Both(_, y) => Some(y), + Self::H(_) => None, + } + } - /// Center-horizon alignment. - pub const CENTER_HORIZON: Axes<Self> = Axes { x: Align::Center, y: Align::Horizon }; + /// Normalize the alignment to a LTR-TTB space. + pub fn fix(self, text_dir: Dir) -> Axes<FixedAlign> { + Axes::new( + self.x().unwrap_or_default().fix(text_dir), + self.y().unwrap_or_default().fix(), + ) + } +} + +#[scope] +impl Align { + pub const START: Self = Align::H(HAlign::Start); + pub const LEFT: Self = Align::H(HAlign::Left); + pub const CENTER: Self = Align::H(HAlign::Center); + pub const RIGHT: Self = Align::H(HAlign::Right); + pub const END: Self = Align::H(HAlign::End); + pub const TOP: Self = Align::V(VAlign::Top); + pub const HORIZON: Self = Align::V(VAlign::Horizon); + pub const BOTTOM: Self = Align::V(VAlign::Bottom); /// The axis this alignment belongs to. - pub const fn axis(self) -> Axis { + /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end` + /// - `{"vertical"}` for `top`, `horizon`, and `bottom` + /// - `{none}` for 2-dimensional alignments + /// + /// ```example + /// #left.axis() \ + /// #bottom.axis() + /// ``` + #[func] + pub const fn axis(self) -> Option<Axis> { match self { - Self::Left | Self::Center | Self::Right => Axis::X, - Self::Top | Self::Horizon | Self::Bottom => Axis::Y, + Self::H(_) => Some(Axis::X), + Self::V(_) => Some(Axis::Y), + Self::Both(..) => None, } } /// The inverse alignment. - pub const fn inv(self) -> Self { + /// + /// ```example + /// #top.inv() \ + /// #left.inv() \ + /// #center.inv() \ + /// #(left + bottom).inv() + /// ``` + #[func(title = "Inverse")] + pub const fn inv(self) -> Align { match self { - Self::Left => Self::Right, - Self::Center => Self::Center, - Self::Right => Self::Left, - Self::Top => Self::Bottom, - Self::Horizon => Self::Horizon, - Self::Bottom => Self::Top, + Self::H(h) => Self::H(h.inv()), + Self::V(v) => Self::V(v.inv()), + Self::Both(h, v) => Self::Both(h.inv(), v.inv()), } } +} - /// Returns the position of this alignment in a container with the given - /// extent. - pub fn position(self, extent: Abs) -> Abs { +impl Default for Align { + fn default() -> Self { + HAlign::default() + VAlign::default() + } +} + +impl Add for Align { + type Output = StrResult<Self>; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::H(x), Self::V(y)) | (Self::V(y), Self::H(x)) => Ok(x + y), + (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"), + (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"), + (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => { + bail!("cannot add a horizontal and a 2D alignment") + } + (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => { + bail!("cannot add a vertical and a 2D alignment") + } + (Self::Both(..), Self::Both(..)) => { + bail!("cannot add two 2D alignments") + } + } + } +} + +impl Debug for Align { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Left | Self::Top => Abs::zero(), - Self::Center | Self::Horizon => extent / 2.0, - Self::Right | Self::Bottom => extent, + Self::H(x) => x.fmt(f), + Self::V(y) => y.fmt(f), + Self::Both(x, y) => write!(f, "{x:?} + {y:?}"), + } + } +} + +impl Fold for Align { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + match (self, outer) { + (Self::H(x), Self::V(y) | Self::Both(_, y)) => Self::Both(x, y), + (Self::V(y), Self::H(x) | Self::Both(x, _)) => Self::Both(x, y), + _ => self, } } } +impl Resolve for Align { + type Output = Axes<FixedAlign>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(item!(dir)(styles)) + } +} + impl From<Side> for Align { fn from(side: Side) -> Self { match side { - Side::Left => Self::Left, - Side::Top => Self::Top, - Side::Right => Self::Right, - Side::Bottom => Self::Bottom, + Side::Left => Self::LEFT, + Side::Top => Self::TOP, + Side::Right => Self::RIGHT, + Side::Bottom => Self::BOTTOM, } } } -impl Debug for Align { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Center => "center", - Self::Right => "right", - Self::Top => "top", - Self::Horizon => "horizon", - Self::Bottom => "bottom", - }) - } +cast! { + type Align, } -/// The generic alignment representation. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum GenAlign { - /// Align at the start side of the text direction. +/// Where to align something horizontally. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub enum HAlign { + #[default] Start, - /// Align at the end side of the text direction. + Left, + Center, + Right, End, - /// Align at a specific alignment. - Specific(Align), } -impl GenAlign { - /// The axis this alignment belongs to. - pub const fn axis(self) -> Axis { - match self { - Self::Start | Self::End => Axis::X, - Self::Specific(align) => align.axis(), - } - } - - /// The inverse alignment. +impl HAlign { + /// The inverse horizontal alignment. pub const fn inv(self) -> Self { match self { Self::Start => Self::End, + Self::Left => Self::Right, + Self::Center => Self::Center, + Self::Right => Self::Left, Self::End => Self::Start, - Self::Specific(align) => Self::Specific(align.inv()), } } -} -impl From<Align> for GenAlign { - fn from(align: Align) -> Self { - Self::Specific(align) + /// Resolve the axis alignment based on the horizontal direction. + pub const fn fix(self, dir: Dir) -> FixedAlign { + match (self, dir.is_positive()) { + (Self::Start, true) | (Self::End, false) => FixedAlign::Start, + (Self::Left, _) => FixedAlign::Start, + (Self::Center, _) => FixedAlign::Center, + (Self::Right, _) => FixedAlign::End, + (Self::End, true) | (Self::Start, false) => FixedAlign::End, + } } } -impl From<HorizontalAlign> for GenAlign { - fn from(align: HorizontalAlign) -> Self { - align.0 +impl Debug for HAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Start => "start", + Self::Left => "left", + Self::Center => "center", + Self::Right => "right", + Self::End => "end", + }) } } -impl From<VerticalAlign> for GenAlign { - fn from(align: VerticalAlign) -> Self { - align.0 +impl Add<VAlign> for HAlign { + type Output = Align; + + fn add(self, rhs: VAlign) -> Self::Output { + Align::Both(self, rhs) } } -impl Debug for GenAlign { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Start => f.pad("start"), - Self::End => f.pad("end"), - Self::Specific(align) => align.fmt(f), - } +impl From<HAlign> for Align { + fn from(align: HAlign) -> Self { + Self::H(align) } } -cast! { - type GenAlign: "alignment", -} +impl Resolve for HAlign { + type Output = FixedAlign; -cast! { - type Axes<GenAlign>: "2d alignment", + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(item!(dir)(styles)) + } } cast! { - Axes<Align>, - self => self.map(GenAlign::from).into_value(), + HAlign, + self => Align::H(self).into_value(), + align: Align => match align { + Align::H(v) => v, + v => bail!("expected `start`, `left`, `center`, `right`, or `end`, found {v:?}"), + } } -cast! { - Axes<Option<GenAlign>>, - self => match (self.x, self.y) { - (Some(x), Some(y)) => Axes::new(x, y).into_value(), - (Some(x), None) => x.into_value(), - (None, Some(y)) => y.into_value(), - (None, None) => Value::None, - }, - align: GenAlign => { - let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(align)); - aligns - }, - aligns: Axes<GenAlign> => aligns.map(Some), +/// Where to align something vertically. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum VAlign { + #[default] + Top, + Horizon, + Bottom, } -impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { - fn from(axes: Axes<GenAlign>) -> Self { - axes.map(Some) +impl VAlign { + /// The inverse vertical alignment. + pub const fn inv(self) -> Self { + match self { + Self::Top => Self::Bottom, + Self::Horizon => Self::Horizon, + Self::Bottom => Self::Top, + } } -} -impl From<Axes<Align>> for Axes<Option<GenAlign>> { - fn from(axes: Axes<Align>) -> Self { - axes.map(GenAlign::Specific).into() + /// Turns into a fixed alignment. + pub const fn fix(self) -> FixedAlign { + match self { + Self::Top => FixedAlign::Start, + Self::Horizon => FixedAlign::Center, + Self::Bottom => FixedAlign::End, + } } } -impl From<Align> for Axes<Option<GenAlign>> { - fn from(align: Align) -> Self { - let mut axes = Axes::splat(None); - axes.set(align.axis(), Some(align.into())); - axes +impl Debug for VAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Top => "top", + Self::Horizon => "horizon", + Self::Bottom => "bottom", + }) } } -impl Resolve for GenAlign { +impl Add<HAlign> for VAlign { type Output = Align; - fn resolve(self, styles: StyleChain) -> Self::Output { - let dir = item!(dir)(styles); - match self { - Self::Start => dir.start().into(), - Self::End => dir.end().into(), - Self::Specific(align) => align, - } + fn add(self, rhs: HAlign) -> Self::Output { + Align::Both(rhs, self) } } -impl Fold for GenAlign { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self +impl From<VAlign> for Align { + fn from(align: VAlign) -> Self { + Self::V(align) } } -impl Fold for Align { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self +cast! { + VAlign, + self => Align::V(self).into_value(), + align: Align => match align { + Align::V(v) => v, + v => bail!("expected `top`, `horizon`, or `bottom`, found {v:?}"), } } -/// Utility struct to restrict a passed alignment value to the horizontal axis -/// on cast. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalAlign(pub GenAlign); +/// A fixed alignment in the global coordinate space. +/// +/// For horizontal alignment, start is globally left and for vertical alignment +/// it is globally top. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum FixedAlign { + Start, + Center, + End, +} -cast! { - HorizontalAlign, - self => self.0.into_value(), - align: GenAlign => { - if align.axis() != Axis::X { - bail!("alignment must be horizontal"); +impl FixedAlign { + /// Returns the position of this alignment in a container with the given + /// extent. + pub fn position(self, extent: Abs) -> Abs { + match self { + Self::Start => Abs::zero(), + Self::Center => extent / 2.0, + Self::End => extent, } - Self(align) - }, + } } -/// Utility struct to restrict a passed alignment value to the vertical axis on -/// cast. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct VerticalAlign(pub GenAlign); +impl Debug for FixedAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Start => "start", + Self::Center => "center", + Self::End => "end", + }) + } +} -cast! { - VerticalAlign, - self => self.0.into_value(), - align: GenAlign => { - if align.axis() != Axis::Y { - bail!("alignment must be vertical"); +impl From<Side> for FixedAlign { + fn from(side: Side) -> Self { + match side { + Side::Left => Self::Start, + Side::Top => Self::Start, + Side::Right => Self::End, + Side::Bottom => Self::End, } - Self(align) - }, + } } diff --git a/crates/typst/src/geom/angle.rs b/crates/typst/src/geom/angle.rs index c03810d9..242c80c9 100644 --- a/crates/typst/src/geom/angle.rs +++ b/crates/typst/src/geom/angle.rs @@ -1,6 +1,17 @@ use super::*; -/// An angle. +/// An angle describing a rotation. +/// +/// Typst supports the following angular units: +/// +/// - Degrees: `{180deg}` +/// - Radians: `{3.14rad}` +/// +/// # Example +/// ```example +/// #rotate(10deg)[Hello there!] +/// ``` +#[ty(scope)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Angle(Scalar); @@ -40,16 +51,6 @@ impl Angle { self.to_raw() / unit.raw_scale() } - /// Convert this to a number of radians. - pub fn to_rad(self) -> f64 { - self.to_unit(AngleUnit::Rad) - } - - /// Convert this to a number of degrees. - pub fn to_deg(self) -> f64 { - self.to_unit(AngleUnit::Deg) - } - /// The absolute value of the this angle. pub fn abs(self) -> Self { Self::raw(self.to_raw().abs()) @@ -71,6 +72,21 @@ impl Angle { } } +#[scope] +impl Angle { + /// Converts this angle to radians. + #[func(name = "rad", title = "Radians")] + pub fn to_rad(self) -> f64 { + self.to_unit(AngleUnit::Rad) + } + + /// Converts this angle to degrees. + #[func(name = "deg", title = "Degrees")] + pub fn to_deg(self) -> f64 { + self.to_unit(AngleUnit::Deg) + } +} + impl Numeric for Angle { fn zero() -> Self { Self::zero() @@ -121,14 +137,6 @@ impl Mul<Angle> for f64 { } } -impl Div<f64> for Angle { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - impl Div for Angle { type Output = f64; @@ -137,6 +145,14 @@ impl Div for Angle { } } +impl Div<f64> for Angle { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + assign_impl!(Angle += Angle); assign_impl!(Angle -= Angle); assign_impl!(Angle *= f64); diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/geom/axes.rs index 1c084888..9384b799 100644 --- a/crates/typst/src/geom/axes.rs +++ b/crates/typst/src/geom/axes.rs @@ -57,6 +57,14 @@ impl<T> Axes<T> { Axes { x: (self.x, other.x), y: (self.y, other.y) } } + /// Apply a function to this and another-instance componentwise. + pub fn zip_map<F, V, U>(self, other: Axes<V>, mut f: F) -> Axes<U> + where + F: FnMut(T, V) -> U, + { + Axes { x: f(self.x, other.x), y: f(self.y, other.y) } + } + /// Whether a condition is true for at least one of fields. pub fn any<F>(self, mut f: F) -> bool where @@ -139,11 +147,7 @@ where T: Debug + 'static, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if let Axes { x: Some(x), y: Some(y) } = - self.as_ref().map(|v| (v as &dyn Any).downcast_ref::<GenAlign>()) - { - write!(f, "{:?} + {:?}", x, y) - } else if (&self.x as &dyn Any).is::<Abs>() { + if (&self.x as &dyn Any).is::<Abs>() { write!(f, "Size({:?}, {:?})", self.x, self.y) } else { write!(f, "Axes({:?}, {:?})", self.x, self.y) @@ -194,6 +198,13 @@ impl Debug for Axis { } } +cast! { + Axis, + self => self.description().into_value(), + "horizontal" => Self::X, + "vertical" => Self::X, +} + impl<T> Axes<Option<T>> { /// Unwrap the individual fields. pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> { @@ -302,7 +313,7 @@ impl<T: Fold> Fold for Axes<Option<T>> { type Output = Axes<T::Output>; fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer).map(|(inner, outer)| match inner { + self.zip_map(outer, |inner, outer| match inner { Some(value) => value.fold(outer), None => outer, }) diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs index ab5aa39e..05565fef 100644 --- a/crates/typst/src/geom/color.rs +++ b/crates/typst/src/geom/color.rs @@ -2,10 +2,32 @@ use ecow::{eco_format, EcoString}; use std::str::FromStr; use super::*; -use crate::diag::bail; -use crate::eval::{cast, Array, Cast}; - -/// A color in a dynamic format. +use crate::diag::{bail, At, SourceResult}; +use crate::eval::{cast, Args, Array, Cast, Func, Str}; +use crate::syntax::Spanned; + +/// A color in a specific color space. +/// +/// Typst supports: +/// - sRGB through the [`rgb` function]($rgb) +/// - Device CMYK through [`cmyk` function]($cmyk) +/// - D65 Gray through the [`luma` function]($luma) +/// +/// Typst provides the following built-in colors: +/// +/// `black`, `gray`, `silver`, `white`, `navy`, `blue`, `aqua`, `teal`, +/// `eastern`, `purple`, `fuchsia`, `maroon`, `red`, `orange`, `yellow`, +/// `olive`, `green`, and `lime`. +/// +/// # Example +/// The predefined colors and the color constructors are available globally and +/// also in the color type's scope, so you can write either of the following +/// two: +/// ```example +/// #rect(fill: aqua) +/// #rect(fill: color.aqua) +/// ``` +#[ty(scope)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Color { /// An 8-bit luma color. @@ -17,6 +39,18 @@ pub enum Color { } impl Color { + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Luma(luma) => luma.to_rgba(), + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } +} + +#[scope] +impl Color { pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); @@ -36,17 +70,172 @@ impl Color { pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { + /// Create a grayscale color. + /// + /// ```example + /// #for x in range(250, step: 50) { + /// box(square(fill: luma(x))) + /// } + /// ``` + #[func] + pub fn luma( + /// The gray component. + gray: Component, + ) -> Color { + LumaColor::new(gray.0).into() + } + + /// Create an RGB(A) color. + /// + /// The color is specified in the sRGB color space. + /// + /// _Note:_ While you can specify transparent colors and Typst's preview will + /// render them correctly, the PDF export does not handle them properly at the + /// moment. This will be fixed in the future. + /// + /// ```example + /// #square(fill: rgb("#b1f2eb")) + /// #square(fill: rgb(87, 127, 230)) + /// #square(fill: rgb(25%, 13%, 65%)) + /// ``` + #[func(title = "RGB")] + pub fn rgb( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: Args, + /// The color in hexadecimal notation. + /// + /// Accepts three, four, six or eight hexadecimal digits and optionally + /// a leading hashtag. + /// + /// If this string is given, the individual components should not be given. + /// + /// ```example + /// #text(16pt, rgb("#239dad"))[ + /// *Typst* + /// ] + /// ``` + #[external] + hex: Str, + /// The red component. + #[external] + red: Component, + /// The green component. + #[external] + green: Component, + /// The blue component. + #[external] + blue: Component, + /// The alpha component. + #[external] + alpha: Component, + ) -> SourceResult<Color> { + let mut args = args; + Ok(if let Some(string) = args.find::<Spanned<Str>>()? { + RgbaColor::from_str(&string.v).at(string.span)?.into() + } else { + let Component(r) = args.expect("red component")?; + let Component(g) = args.expect("green component")?; + let Component(b) = args.expect("blue component")?; + let Component(a) = args.eat()?.unwrap_or(Component(255)); + RgbaColor::new(r, g, b, a).into() + }) + } + + /// Create a CMYK color. + /// + /// This is useful if you want to target a specific printer. The conversion + /// to RGB for display preview might differ from how your printer reproduces + /// the color. + /// + /// ```example + /// #square( + /// fill: cmyk(27%, 0%, 3%, 5%) + /// ) + /// ``` + #[func(title = "CMYK")] + pub fn cmyk( + /// The cyan component. + cyan: RatioComponent, + /// The magenta component. + magenta: RatioComponent, + /// The yellow component. + yellow: RatioComponent, + /// The key component. + key: RatioComponent, + ) -> Color { + CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into() + } + + /// Returns the constructor function for this color's kind + /// ([`rgb`]($color.rgb), [`cmyk`]($color.cmyk) or [`luma`]($color.luma)). + /// + /// ```example + /// #let color = cmyk(1%, 2%, 3%, 4%) + /// #(color.kind() == cmyk) + /// ``` + #[func] + pub fn kind(self) -> Func { match self { - Self::Luma(luma) => luma.to_rgba(), - Self::Rgba(rgba) => rgba, - Self::Cmyk(cmyk) => cmyk.to_rgba(), + Self::Luma(_) => Self::luma_data().into(), + Self::Rgba(_) => Self::rgb_data().into(), + Self::Cmyk(_) => Self::cmyk_data().into(), } } - /// Lighten this color by the given factor. - pub fn lighten(self, factor: Ratio) -> Self { + /// Returns the color's RGB(A) hex representation (such as `#ffaa32` or + /// `#020304fe`). The alpha component (last two digits in `#020304fe`) is + /// omitted if it is equal to `ff` (255 / 100%). + #[func] + pub fn to_hex(self) -> EcoString { + self.to_rgba().to_hex() + } + + /// Converts this color to sRGB and returns its components (R, G, B, A) as + /// an array of [integers]($int). + #[func(name = "to-rgba")] + pub fn to_rgba_array(self) -> Array { + self.to_rgba().to_array() + } + + /// Converts this color to Digital CMYK and returns its components + /// (C, M, Y, K) as an array of [ratios]($ratio). Note that this function + /// will throw an error when applied to an [rgb]($rgb) color, since its + /// conversion to CMYK is not available. + #[func] + pub fn to_cmyk(self) -> StrResult<Array> { + match self { + Self::Luma(luma) => Ok(luma.to_cmyk().to_array()), + Self::Rgba(_) => { + bail!("cannot obtain cmyk values from rgba color") + } + Self::Cmyk(cmyk) => Ok(cmyk.to_array()), + } + } + + /// If this color was created with [luma]($luma), returns the + /// [integer]($int) value used on construction. Otherwise (for [rgb]($rgb) + /// and [cmyk]($cmyk) colors), throws an error. + #[func] + pub fn to_luma(self) -> StrResult<u8> { + match self { + Self::Luma(luma) => Ok(luma.0), + Self::Rgba(_) => { + bail!("cannot obtain the luma value of rgba color") + } + Self::Cmyk(_) => { + bail!("cannot obtain the luma value of cmyk color") + } + } + } + + /// Lightens a color by a given factor. + #[func] + pub fn lighten( + self, + /// The factor to lighten the color by. + factor: Ratio, + ) -> Color { match self { Self::Luma(luma) => Self::Luma(luma.lighten(factor)), Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), @@ -54,8 +243,13 @@ impl Color { } } - /// Darken this color by the given factor. - pub fn darken(self, factor: Ratio) -> Self { + /// Darkens a color by a given factor. + #[func] + pub fn darken( + self, + /// The factor to darken the color by. + factor: Ratio, + ) -> Color { match self { Self::Luma(luma) => Self::Luma(luma.darken(factor)), Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), @@ -63,8 +257,9 @@ impl Color { } } - /// Negate this color. - pub fn negate(self) -> Self { + /// Produces the negative of the color. + #[func] + pub fn negate(self) -> Color { match self { Self::Luma(luma) => Self::Luma(luma.negate()), Self::Rgba(rgba) => Self::Rgba(rgba.negate()), @@ -72,9 +267,28 @@ impl Color { } } - /// Mixes multiple colors through weight. + /// Create a color by mixing two or more colors. + /// + /// ```example + /// #set block(height: 20pt, width: 100%) + /// #block(fill: red.mix(blue)) + /// #block(fill: red.mix(blue, space: "srgb")) + /// #block(fill: color.mix(red, blue, white)) + /// #block(fill: color.mix((red, 70%), (blue, 30%))) + /// ``` + #[func] pub fn mix( - colors: impl IntoIterator<Item = WeightedColor>, + /// The colors, optionally with weights, specified as a pair (array of + /// length two) of color and weight (float or ratio). + /// + /// The weights do not need to add to `{100%}`, they are relative to the + /// sum of all weights. + #[variadic] + colors: Vec<WeightedColor>, + /// The color space to mix in. By default, this happens in a perceptual + /// color space (Oklab). + #[named] + #[default(ColorSpace::Oklab)] space: ColorSpace, ) -> StrResult<Color> { let mut total = 0.0; @@ -465,6 +679,34 @@ cast! { self => Value::Color(self.into()), } +/// An integer or ratio component. +pub struct Component(u8); + +cast! { + Component, + v: i64 => match v { + 0 ..= 255 => Self(v as u8), + _ => bail!("number must be between 0 and 255"), + }, + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + +/// A component that must be a ratio. +pub struct RatioComponent(u8); + +cast! { + RatioComponent, + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + /// Convert to the closest u8. fn round_u8(value: f64) -> u8 { value.round() as u8 diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/geom/corners.rs index 5ee1e063..d21dc22e 100644 --- a/crates/typst/src/geom/corners.rs +++ b/crates/typst/src/geom/corners.rs @@ -111,8 +111,12 @@ pub enum Corner { } impl<T: Reflect> Reflect for Corners<Option<T>> { - fn describe() -> CastInfo { - T::describe() + Dict::describe() + fn input() -> CastInfo { + T::input() + Dict::input() + } + + fn output() -> CastInfo { + T::output() + Dict::output() } fn castable(value: &Value) -> bool { diff --git a/crates/typst/src/geom/dir.rs b/crates/typst/src/geom/dir.rs index 48915471..897d7769 100644 --- a/crates/typst/src/geom/dir.rs +++ b/crates/typst/src/geom/dir.rs @@ -1,6 +1,21 @@ use super::*; /// The four directions into which content can be laid out. +/// +/// Possible values are: +/// - `{ltr}`: Left to right. +/// - `{rtl}`: Right to left. +/// - `{ttb}`: Top to bottom. +/// - `{btt}`: Bottom to top. +/// +/// These values are available globally and +/// also in the direction type's scope, so you can write either of the following +/// two: +/// ```example +/// #stack(dir: rtl)[A][B][C] +/// #stack(dir: direction.rtl)[A][B][C] +/// ``` +#[ty(scope, name = "direction")] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Dir { /// Left to right. @@ -14,7 +29,32 @@ pub enum Dir { } impl Dir { - /// The specific axis this direction belongs to. + /// Whether this direction points into the positive coordinate direction. + /// + /// The positive directions are left-to-right and top-to-bottom. + pub const fn is_positive(self) -> bool { + match self { + Self::LTR | Self::TTB => true, + Self::RTL | Self::BTT => false, + } + } +} + +#[scope] +impl Dir { + pub const LTR: Self = Self::LTR; + pub const RTL: Self = Self::RTL; + pub const TTB: Self = Self::TTB; + pub const BTT: Self = Self::BTT; + + /// The axis this direction belongs to, either `{"horizontal"}` or + /// `{"vertical"}`. + /// + /// ```example + /// #ltr.axis() \ + /// #ttb.axis() + /// ``` + #[func] pub const fn axis(self) -> Axis { match self { Self::LTR | Self::RTL => Axis::X, @@ -22,7 +62,15 @@ impl Dir { } } - /// The side this direction starts at. + /// The start point of this direction, as an alignment. + /// + /// ```example + /// #ltr.start() \ + /// #rtl.start() \ + /// #ttb.start() \ + /// #btt.start() + /// ``` + #[func] pub const fn start(self) -> Side { match self { Self::LTR => Side::Left, @@ -32,7 +80,15 @@ impl Dir { } } - /// The side this direction ends at. + /// The end point of this direction, as an alignment. + /// + /// ```example + /// #ltr.end() \ + /// #rtl.end() \ + /// #ttb.end() \ + /// #btt.end() + /// ``` + #[func] pub const fn end(self) -> Side { match self { Self::LTR => Side::Right, @@ -43,7 +99,15 @@ impl Dir { } /// The inverse direction. - pub const fn inv(self) -> Self { + /// + /// ```example + /// #ltr.inv() \ + /// #rtl.inv() \ + /// #ttb.inv() \ + /// #btt.inv() + /// ``` + #[func(title = "Inverse")] + pub const fn inv(self) -> Dir { match self { Self::LTR => Self::RTL, Self::RTL => Self::LTR, @@ -51,16 +115,6 @@ impl Dir { Self::BTT => Self::TTB, } } - - /// Whether this direction points into the positive coordinate direction. - /// - /// The positive directions are left-to-right and top-to-bottom. - pub const fn is_positive(self) -> bool { - match self { - Self::LTR | Self::TTB => true, - Self::RTL | Self::BTT => false, - } - } } impl Debug for Dir { @@ -75,5 +129,5 @@ impl Debug for Dir { } cast! { - type Dir: "direction", + type Dir, } diff --git a/crates/typst/src/geom/ellipse.rs b/crates/typst/src/geom/ellipse.rs index ac20ffd3..36046d95 100644 --- a/crates/typst/src/geom/ellipse.rs +++ b/crates/typst/src/geom/ellipse.rs @@ -1,7 +1,7 @@ use super::*; /// Produce a shape that approximates an axis-aligned ellipse. -pub fn ellipse(size: Size, fill: Option<Paint>, stroke: Option<Stroke>) -> Shape { +pub fn ellipse(size: Size, fill: Option<Paint>, stroke: Option<FixedStroke>) -> Shape { // https://stackoverflow.com/a/2007782 let z = Abs::zero(); let rx = size.x / 2.0; diff --git a/crates/typst/src/geom/fr.rs b/crates/typst/src/geom/fr.rs index c602634d..f7cec5d7 100644 --- a/crates/typst/src/geom/fr.rs +++ b/crates/typst/src/geom/fr.rs @@ -1,6 +1,18 @@ use super::*; -/// A fraction of remaining space. +/// Defines how the the remaining space in a layout is distributed. +/// +/// Each fractionally sized element gets space based on the ratio of its +/// fraction to the sum of all fractions. +/// +/// For more details, also see the [h]($h) and [v]($v) functions and the +/// [grid function]($grid). +/// +/// # Example +/// ```example +/// Left #h(1fr) Left-ish #h(2fr) Right +/// ``` +#[ty(name = "fraction")] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Fr(Scalar); @@ -91,14 +103,6 @@ impl Mul<Fr> for f64 { } } -impl Div<f64> for Fr { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - impl Div for Fr { type Output = f64; @@ -107,6 +111,14 @@ impl Div for Fr { } } +impl Div<f64> for Fr { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + assign_impl!(Fr += Fr); assign_impl!(Fr -= Fr); assign_impl!(Fr *= f64); diff --git a/crates/typst/src/geom/length.rs b/crates/typst/src/geom/length.rs index ccd5362c..453dbe59 100644 --- a/crates/typst/src/geom/length.rs +++ b/crates/typst/src/geom/length.rs @@ -1,9 +1,38 @@ +use ecow::eco_format; + use super::*; +use crate::diag::{At, Hint, SourceResult}; +use crate::syntax::Span; /// A size or distance, possibly expressed with contextual units. /// -/// Currently supports absolute and font-relative units, but support could quite -/// easily be extended to other units. +/// Typst supports the following length units: +/// +/// - Points: `{72pt}` +/// - Millimeters: `{254mm}` +/// - Centimeters: `{2.54cm}` +/// - Inches: `{1in}` +/// - Relative to font size: `{2.5em}` +/// +/// You can multiply lengths with and divide them by integers and floats. +/// +/// # Example +/// ```example +/// #rect(width: 20pt) +/// #rect(width: 2em) +/// #rect(width: 1in) +/// +/// #(3em + 5pt).em \ +/// #(20pt).em \ +/// #(40em + 2pt).abs \ +/// #(5em).abs +/// ``` +/// +/// # Fields +/// - `abs`: A length with just the absolute component of the current length +/// (that is, excluding the `em` component). +/// - `em`: The amount of `em` units in this length, as a [float]($float). +#[ty(scope)] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Length { /// The absolute part. @@ -39,6 +68,63 @@ impl Length { pub fn at(self, font_size: Abs) -> Abs { self.abs + self.em.at(font_size) } + + /// Fails with an error if the length has a non-zero font-relative part. + fn ensure_that_em_is_zero(&self, span: Span, unit: &str) -> SourceResult<()> { + if self.em == Em::zero() { + return Ok(()); + } + Err(eco_format!( + "cannot convert a length with non-zero em units (`{self:?}`) to {unit}" + )) + .hint(eco_format!("use `length.abs.{unit}()` instead to ignore its em component")) + .at(span) + } +} + +#[scope] +impl Length { + /// Converts this length to points. + /// + /// Fails with an error if this length has non-zero `em` units (such as + /// `5em + 2pt` instead of just `2pt`). Use the `abs` field (such as in + /// `(5em + 2pt).abs.pt()`) to ignore the `em` component of the length (thus + /// converting only its absolute component). + #[func(name = "pt", title = "Points")] + pub fn to_pt(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "pt")?; + Ok(self.abs.to_pt()) + } + + /// Converts this length to millimeters. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "mm", title = "Millimeters")] + pub fn to_mm(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "mm")?; + Ok(self.abs.to_mm()) + } + + /// Converts this length to centimeters. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "cm", title = "Centimeters")] + pub fn to_cm(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "cm")?; + Ok(self.abs.to_cm()) + } + + /// Converts this length to inches. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "inches")] + pub fn to_inches(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "inches")?; + Ok(self.abs.to_inches()) + } } impl Debug for Length { @@ -111,6 +197,14 @@ impl Mul<f64> for Length { } } +impl Mul<Length> for f64 { + type Output = Length; + + fn mul(self, rhs: Length) -> Self::Output { + rhs * self + } +} + impl Div<f64> for Length { type Output = Self; diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs index 922d25d7..b6ccfb3a 100644 --- a/crates/typst/src/geom/mod.rs +++ b/crates/typst/src/geom/mod.rs @@ -28,7 +28,7 @@ mod stroke; mod transform; pub use self::abs::{Abs, AbsUnit}; -pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; +pub use self::align::{Align, FixedAlign, HAlign, VAlign}; pub use self::angle::{Angle, AngleUnit}; pub use self::axes::{Axes, Axis}; pub use self::color::{ @@ -51,9 +51,7 @@ pub use self::shape::{Geometry, Shape}; pub use self::sides::{Side, Sides}; pub use self::size::Size; pub use self::smart::Smart; -pub use self::stroke::{ - DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, -}; +pub use self::stroke::{DashLength, DashPattern, FixedStroke, LineCap, LineJoin, Stroke}; pub use self::transform::Transform; use std::cmp::Ordering; @@ -64,7 +62,7 @@ use std::iter::Sum; use std::ops::*; use crate::diag::{bail, StrResult}; -use crate::eval::{array, cast, Array, Dict, Value}; +use crate::eval::{array, cast, func, scope, ty, Array, Dict, Value}; use crate::model::{Fold, Resolve, StyleChain}; /// Generic access to a structure's components. diff --git a/crates/typst/src/geom/ratio.rs b/crates/typst/src/geom/ratio.rs index fe87dd6c..76a09006 100644 --- a/crates/typst/src/geom/ratio.rs +++ b/crates/typst/src/geom/ratio.rs @@ -2,8 +2,16 @@ use super::*; /// A ratio of a whole. /// -/// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the -/// corresponding [literal](crate::syntax::ast::Numeric). +/// Written as a number, followed by a percent sign. +/// +/// # Example +/// ```example +/// #set align(center) +/// #scale(x: 150%)[ +/// Scaled apart. +/// ] +/// ``` +#[ty] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ratio(Scalar); @@ -102,6 +110,14 @@ impl Mul<Ratio> for f64 { } } +impl Div for Ratio { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + impl Div<f64> for Ratio { type Output = Self; @@ -118,14 +134,6 @@ impl Div<Ratio> for f64 { } } -impl Div for Ratio { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.get() / other.get() - } -} - assign_impl!(Ratio += Ratio); assign_impl!(Ratio -= Ratio); assign_impl!(Ratio *= Ratio); diff --git a/crates/typst/src/geom/rel.rs b/crates/typst/src/geom/rel.rs index 88972222..59a1348d 100644 --- a/crates/typst/src/geom/rel.rs +++ b/crates/typst/src/geom/rel.rs @@ -1,8 +1,25 @@ use super::*; -/// A value that is composed of a relative and an absolute part. +/// A length in relation to some known length. +/// +/// This type is a combination of a [length]($length) with a [ratio]($ratio). It +/// results from addition and subtraction of a length and a ratio. Wherever a +/// relative length is expected, you can also use a bare length or ratio. +/// +/// # Example +/// ```example +/// #rect(width: 100% - 50pt) +/// +/// #(100% - 50pt).length \ +/// #(100% - 50pt).ratio +/// ``` +/// +/// A relative length has the following fields: +/// - `length`: Its length component. +/// - `ratio`: Its ratio component. +#[ty(name = "relative", title = "Relative Length")] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Rel<T: Numeric> { +pub struct Rel<T: Numeric = Length> { /// The relative part. pub rel: Ratio, /// The absolute part. diff --git a/crates/typst/src/geom/rounded.rs b/crates/typst/src/geom/rounded.rs index f1a7ea08..abaf46de 100644 --- a/crates/typst/src/geom/rounded.rs +++ b/crates/typst/src/geom/rounded.rs @@ -5,7 +5,7 @@ pub fn rounded_rect( size: Size, radius: Corners<Abs>, fill: Option<Paint>, - stroke: Sides<Option<Stroke>>, + stroke: Sides<Option<FixedStroke>>, ) -> Vec<Shape> { let mut res = vec![]; if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { @@ -43,8 +43,8 @@ fn fill_geometry(size: Size, radius: Corners<Abs>) -> Geometry { fn stroke_segments( size: Size, radius: Corners<Abs>, - stroke: Sides<Option<Stroke>>, -) -> Vec<(Path, Option<Stroke>)> { + stroke: Sides<Option<FixedStroke>>, +) -> Vec<(Path, Option<FixedStroke>)> { let mut res = vec![]; let mut connection = Connection::default(); diff --git a/crates/typst/src/geom/shape.rs b/crates/typst/src/geom/shape.rs index 5658c21f..0681e3b2 100644 --- a/crates/typst/src/geom/shape.rs +++ b/crates/typst/src/geom/shape.rs @@ -8,7 +8,7 @@ pub struct Shape { /// The shape's background fill. pub fill: Option<Paint>, /// The shape's border stroke. - pub stroke: Option<Stroke>, + pub stroke: Option<FixedStroke>, } /// A shape's geometry. @@ -29,7 +29,7 @@ impl Geometry { } /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: Stroke) -> Shape { + pub fn stroked(self, stroke: FixedStroke) -> Shape { Shape { geometry: self, fill: None, stroke: Some(stroke) } } } diff --git a/crates/typst/src/geom/sides.rs b/crates/typst/src/geom/sides.rs index d4b72a9d..c1c2dadf 100644 --- a/crates/typst/src/geom/sides.rs +++ b/crates/typst/src/geom/sides.rs @@ -180,9 +180,25 @@ impl Side { } } +cast! { + Side, + self => Align::from(self).into_value(), + align: Align => match align { + Align::LEFT => Self::Left, + Align::RIGHT => Self::Right, + Align::TOP => Self::Top, + Align::BOTTOM => Self::Bottom, + _ => bail!("cannot convert this alignment to a side"), + }, +} + impl<T: Reflect> Reflect for Sides<Option<T>> { - fn describe() -> CastInfo { - T::describe() + Dict::describe() + fn input() -> CastInfo { + T::input() + Dict::input() + } + + fn output() -> CastInfo { + T::output() + Dict::output() } fn castable(value: &Value) -> bool { diff --git a/crates/typst/src/geom/smart.rs b/crates/typst/src/geom/smart.rs index a6271c20..2c6e241e 100644 --- a/crates/typst/src/geom/smart.rs +++ b/crates/typst/src/geom/smart.rs @@ -97,12 +97,16 @@ impl<T> Default for Smart<T> { } impl<T: Reflect> Reflect for Smart<T> { - fn castable(value: &Value) -> bool { - AutoValue::castable(value) || T::castable(value) + fn input() -> CastInfo { + T::input() + AutoValue::input() + } + + fn output() -> CastInfo { + T::output() + AutoValue::output() } - fn describe() -> CastInfo { - T::describe() + AutoValue::describe() + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) } } diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/geom/stroke.rs index b0387fb7..820f4f44 100644 --- a/crates/typst/src/geom/stroke.rs +++ b/crates/typst/src/geom/stroke.rs @@ -2,43 +2,73 @@ use crate::eval::{dict, Cast, FromValue, NoneValue}; use super::*; -/// A stroke of a geometric shape. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Stroke { - /// The stroke's paint. - pub paint: Paint, - /// The stroke's thickness. - pub thickness: Abs, - /// The stroke's line cap. - pub line_cap: LineCap, - /// The stroke's line join. - pub line_join: LineJoin, - /// The stroke's line dash pattern. - pub dash_pattern: Option<DashPattern<Abs, Abs>>, - /// The miter limit. Defaults to 4.0, same as `tiny-skia`. - pub miter_limit: Scalar, -} - -impl Default for Stroke { - fn default() -> Self { - Self { - paint: Paint::Solid(Color::BLACK), - thickness: Abs::pt(1.0), - line_cap: LineCap::Butt, - line_join: LineJoin::Miter, - dash_pattern: None, - miter_limit: Scalar(4.0), - } - } -} - -/// A partial stroke representation. +/// Defines how to draw a line. +/// +/// A stroke has a _paint_ (typically a solid color), a _thickness,_ a line +/// _cap,_ a line _join,_ a _miter-limit,_ and a _dash_ pattern. All of these +/// values are optional and have sensible defaults. +/// +/// # Example +/// ```example +/// #set line(length: 100%) +/// #stack( +/// spacing: 1em, +/// line(stroke: 2pt + red), +/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")), +/// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")), +/// ) +/// ``` +/// +/// # Simple strokes +/// You can create a simple solid stroke from a color, a thickness, or a +/// combination of the two. Specifically, wherever a stroke is expected you can +/// pass any of the following values: +/// +/// - A length specifying the stroke's thickness. The color is inherited, +/// defaulting to black. +/// - A color to use for the stroke. The thickness is inherited, defaulting to +/// `{1pt}`. +/// - A stroke combined from color and thickness using the `+` operator as in +/// `{2pt + red}`. /// -/// In this representation, both fields are optional so that you can pass either -/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where -/// this is expected. +/// # Complex strokes +/// For full control, you can also pass a [dictionary]($dictionary) to any +/// function that expects a stroke. This dictionary has the following keys: +/// +/// - `paint`: The [color]($color) to use for the stroke. +/// +/// - `thickness`: The stroke's thickness as a [length]($length). +/// +/// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or +/// `{"square"}`. +/// +/// - `join`: How sharp turns of a contour are rendered. One of `{"miter"}`, +/// `{"round"}`, or `{"bevel"}`. Not applicable to lines but to +/// [polygons]($polygon) or [paths]($path). +/// +/// - `miter-limit`: Number at which protruding sharp angles are rendered with a +/// bevel instead. The higher the number, the sharper an angle can be before +/// it is bevelled. Only applicable if `join` is `{"miter"}`. Defaults to +/// `{4.0}`. +/// +/// - `dash`: The dash pattern to use. Can be any of the following: +/// - One of the predefined patterns `{"solid"}`, `{"dotted"}`, +/// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`, +/// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`, +/// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}` +/// - An [array]($array) with alternating lengths for dashes and gaps. You can +/// also use the string `{"dot"}` for a length equal to the line thickness. +/// - A [dictionary]($dictionary) with the keys `array` (same as the array +/// above), and `phase` (of type [length]($length)), which defines where in +/// the pattern to start drawing. +/// +/// # Fields +/// On a `stroke` object, you can access any of the fields mentioned in the +/// dictionary format above. For example, `{(2pt + blue).thickness}` is `{2pt}`, +/// `{(2pt + blue).miter-limit}` is `{4.0}` (the default), and so on. +#[ty] #[derive(Default, Clone, Eq, PartialEq, Hash)] -pub struct PartialStroke<T = Length> { +pub struct Stroke<T: Numeric = Length> { /// The stroke's paint. pub paint: Smart<Paint>, /// The stroke's thickness. @@ -53,13 +83,13 @@ pub struct PartialStroke<T = Length> { pub miter_limit: Smart<Scalar>, } -impl<T> PartialStroke<T> { +impl<T: Numeric> Stroke<T> { /// Map the contained lengths with `f`. - pub fn map<F, U>(self, f: F) -> PartialStroke<U> + pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U> where F: Fn(T) -> U, { - PartialStroke { + Stroke { paint: self.paint, thickness: self.thickness.map(&f), line_cap: self.line_cap, @@ -82,9 +112,9 @@ impl<T> PartialStroke<T> { } } -impl PartialStroke<Abs> { +impl Stroke<Abs> { /// Unpack the stroke, filling missing fields from the `default`. - pub fn unwrap_or(self, default: Stroke) -> Stroke { + pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke { let thickness = self.thickness.unwrap_or(default.thickness); let dash_pattern = self .dash_pattern @@ -100,7 +130,7 @@ impl PartialStroke<Abs> { }) .unwrap_or(default.dash_pattern); - Stroke { + FixedStroke { paint: self.paint.unwrap_or(default.paint), thickness, line_cap: self.line_cap.unwrap_or(default.line_cap), @@ -111,12 +141,12 @@ impl PartialStroke<Abs> { } /// Unpack the stroke, filling missing fields with the default values. - pub fn unwrap_or_default(self) -> Stroke { - self.unwrap_or(Stroke::default()) + pub fn unwrap_or_default(self) -> FixedStroke { + self.unwrap_or(FixedStroke::default()) } } -impl<T: Debug> Debug for PartialStroke<T> { +impl<T: Numeric + Debug> Debug for Stroke<T> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let Self { paint, @@ -176,11 +206,11 @@ impl<T: Debug> Debug for PartialStroke<T> { } } -impl Resolve for PartialStroke { - type Output = PartialStroke<Abs>; +impl Resolve for Stroke { + type Output = Stroke<Abs>; fn resolve(self, styles: StyleChain) -> Self::Output { - PartialStroke { + Stroke { paint: self.paint, thickness: self.thickness.resolve(styles), line_cap: self.line_cap, @@ -191,7 +221,7 @@ impl Resolve for PartialStroke { } } -impl Fold for PartialStroke<Abs> { +impl Fold for Stroke<Abs> { type Output = Self; fn fold(self, outer: Self::Output) -> Self::Output { @@ -207,7 +237,7 @@ impl Fold for PartialStroke<Abs> { } cast! { - type PartialStroke: "stroke", + type Stroke, thickness: Length => Self { thickness: Smart::Custom(thickness), ..Default::default() @@ -242,7 +272,7 @@ cast! { } cast! { - PartialStroke<Abs>, + Stroke<Abs>, self => self.map(Length::from).into_value(), } @@ -284,14 +314,14 @@ impl Debug for LineJoin { /// A line dash pattern. #[derive(Clone, Eq, PartialEq, Hash)] -pub struct DashPattern<T = Length, DT = DashLength<T>> { +pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> { /// The dash array. pub array: Vec<DT>, /// The dash phase. pub phase: T, } -impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> { +impl<T: Numeric + Debug, DT: Debug> Debug for DashPattern<T, DT> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "(array: (")?; for (i, elem) in self.array.iter().enumerate() { @@ -306,7 +336,7 @@ impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> { } } -impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { +impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> { fn from(array: Vec<DashLength<T>>) -> Self { Self { array, phase: T::default() } } @@ -353,20 +383,14 @@ cast! { }, } -/// The length of a dash in a line dash pattern +/// The length of a dash in a line dash pattern. #[derive(Clone, Eq, PartialEq, Hash)] -pub enum DashLength<T = Length> { +pub enum DashLength<T: Numeric = Length> { LineWidth, Length(T), } -impl From<Abs> for DashLength { - fn from(l: Abs) -> Self { - DashLength::Length(l.into()) - } -} - -impl<T> DashLength<T> { +impl<T: Numeric> DashLength<T> { fn finish(self, line_width: T) -> T { match self { Self::LineWidth => line_width, @@ -375,7 +399,7 @@ impl<T> DashLength<T> { } } -impl<T: Debug> Debug for DashLength<T> { +impl<T: Numeric + Debug> Debug for DashLength<T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::LineWidth => write!(f, "\"dot\""), @@ -395,13 +419,48 @@ impl Resolve for DashLength { } } +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + cast! { DashLength, self => match self { Self::LineWidth => "dot".into_value(), Self::Length(v) => v.into_value(), }, - "dot" => Self::LineWidth, v: Length => Self::Length(v), } + +/// A fully specified stroke of a geometric shape. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FixedStroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Abs, + /// The stroke's line cap. + pub line_cap: LineCap, + /// The stroke's line join. + pub line_join: LineJoin, + /// The stroke's line dash pattern. + pub dash_pattern: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, +} + +impl Default for FixedStroke { + fn default() -> Self { + Self { + paint: Paint::Solid(Color::BLACK), + thickness: Abs::pt(1.0), + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + dash_pattern: None, + miter_limit: Scalar(4.0), + } + } +} diff --git a/crates/typst/src/ide/complete.rs b/crates/typst/src/ide/complete.rs index 4f2bacfd..f100de8d 100644 --- a/crates/typst/src/ide/complete.rs +++ b/crates/typst/src/ide/complete.rs @@ -10,8 +10,9 @@ use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use crate::doc::Frame; use crate::eval::{ - fields_on, format_str, methods_on, CastInfo, Func, Library, Plugin, Scope, Value, + format_str, AutoValue, CastInfo, Func, Library, NoneValue, Scope, Type, Value, }; +use crate::geom::Color; use crate::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, }; @@ -70,6 +71,8 @@ pub enum CompletionKind { Syntax, /// A function. Func, + /// A type. + Type, /// A function parameter. Param, /// A constant. @@ -352,7 +355,17 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { /// Add completions for all fields on a value. fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { - for &(method, args) in methods_on(value.type_name()) { + for (name, value) in value.ty().scope().iter() { + ctx.value_completion(Some(name.clone()), value, true, None); + } + + if let Some(scope) = value.scope() { + for (name, value) in scope.iter() { + ctx.value_completion(Some(name.clone()), value, true, None); + } + } + + for &(method, args) in crate::eval::mutable_methods_on(value.ty()) { ctx.completions.push(Completion { kind: CompletionKind::Func, label: method.into(), @@ -365,7 +378,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { }) } - for &field in fields_on(value.type_name()) { + for &field in crate::eval::fields_on(value.ty()) { // Complete the field name along with its value. Notes: // 1. No parentheses since function fields cannot currently be called // with method syntax; @@ -394,7 +407,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } Value::Content(content) => { for (name, value) in content.fields() { - ctx.value_completion(Some(name.clone()), &value, false, None); + ctx.value_completion(Some(name.into()), &value, false, None); } } Value::Dict(dict) => { @@ -402,29 +415,14 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { ctx.value_completion(Some(name.clone().into()), value, false, None); } } - Value::Module(module) => { - for (name, value) in module.scope().iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - Value::Func(func) => { - if let Some(info) = func.info() { - // Consider all names from the function's scope. - for (name, value) in info.scope.iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - } - Value::Dyn(val) => { - if let Some(plugin) = val.downcast::<Plugin>() { - for name in plugin.iter() { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: name.clone(), - apply: None, - detail: None, - }) - } + Value::Plugin(plugin) => { + for name in plugin.iter() { + ctx.completions.push(Completion { + kind: CompletionKind::Func, + label: name.clone(), + apply: None, + detail: None, + }) } } _ => {} @@ -557,9 +555,10 @@ fn set_rule_completions(ctx: &mut CompletionContext) { ctx.scope_completions(true, |value| { matches!( value, - Value::Func(func) if func.info().map_or(false, |info| { - info.params.iter().any(|param| param.settable) - }), + Value::Func(func) if func.params() + .unwrap_or_default() + .iter() + .any(|param| param.settable), ) }); } @@ -693,9 +692,9 @@ fn param_completions<'a>( exclude: &[ast::Ident<'a>], ) { let Some(func) = resolve_global_callee(ctx, callee) else { return }; - let Some(info) = func.info() else { return }; + let Some(params) = func.params() else { return }; - for param in &info.params { + for param in params { if exclude.iter().any(|ident| ident.as_str() == param.name) { continue; } @@ -714,7 +713,7 @@ fn param_completions<'a>( } if param.positional { - ctx.cast_completions(¶m.cast); + ctx.cast_completions(¶m.input); } } @@ -730,13 +729,12 @@ fn named_param_value_completions<'a>( name: &str, ) { let Some(func) = resolve_global_callee(ctx, callee) else { return }; - let Some(info) = func.info() else { return }; - let Some(param) = info.param(name) else { return }; + let Some(param) = func.param(name) else { return }; if !param.named { return; } - ctx.cast_completions(¶m.cast); + ctx.cast_completions(¶m.input); if name == "font" { ctx.font_completions(); } @@ -755,8 +753,8 @@ fn resolve_global_callee<'a>( ast::Expr::Ident(ident) => ctx.global.get(&ident)?, ast::Expr::FieldAccess(access) => match access.target() { ast::Expr::Ident(target) => match ctx.global.get(&target)? { - Value::Module(module) => module.get(&access.field()).ok()?, - Value::Func(func) => func.get(&access.field()).ok()?, + Value::Module(module) => module.field(&access.field()).ok()?, + Value::Func(func) => func.field(&access.field()).ok()?, _ => return None, }, _ => return None, @@ -808,7 +806,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { #[rustfmt::skip] fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { ctx.scope_completions(true, |value| !hashtag || { - matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Module(_)) + matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)) }); ctx.snippet_completion( @@ -1105,7 +1103,7 @@ impl<'a> CompletionContext<'a> { let detail = docs.map(Into::into).or_else(|| match value { Value::Symbol(_) => None, - Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)), + Value::Func(func) => func.docs().map(plain_docs_sentence), v => { let repr = v.repr(); (repr.as_str() != label).then(|| repr.into()) @@ -1114,7 +1112,16 @@ impl<'a> CompletionContext<'a> { let mut apply = None; if parens && matches!(value, Value::Func(_)) { - apply = Some(eco_format!("{label}(${{}})")); + if let Value::Func(func) = value { + if func + .params() + .is_some_and(|params| params.iter().all(|param| param.name == "self")) + { + apply = Some(eco_format!("{label}()${{}}")); + } else { + apply = Some(eco_format!("{label}(${{}})")); + } + } } else if at { apply = Some(eco_format!("at(\"{label}\")")); } else if label.starts_with('"') && self.after.starts_with('"') { @@ -1126,6 +1133,7 @@ impl<'a> CompletionContext<'a> { self.completions.push(Completion { kind: match value { Value::Func(_) => CompletionKind::Func, + Value::Type(_) => CompletionKind::Type, Value::Symbol(s) => CompletionKind::Symbol(s.get()), _ => CompletionKind::Constant, }, @@ -1147,47 +1155,46 @@ impl<'a> CompletionContext<'a> { CastInfo::Value(value, docs) => { self.value_completion(None, value, true, Some(docs)); } - CastInfo::Type("none") => self.snippet_completion("none", "none", "Nothing."), - CastInfo::Type("auto") => { - self.snippet_completion("auto", "auto", "A smart default."); - } - CastInfo::Type("boolean") => { - self.snippet_completion("false", "false", "No / Disabled."); - self.snippet_completion("true", "true", "Yes / Enabled."); - } - CastInfo::Type("color") => { - self.snippet_completion( - "luma()", - "luma(${v})", - "A custom grayscale color.", - ); - self.snippet_completion( - "rgb()", - "rgb(${r}, ${g}, ${b}, ${a})", - "A custom RGBA color.", - ); - self.snippet_completion( - "cmyk()", - "cmyk(${c}, ${m}, ${y}, ${k})", - "A custom CMYK color.", - ); - self.scope_completions(false, |value| value.type_name() == "color"); - } - CastInfo::Type("function") => { - self.snippet_completion( - "function", - "(${params}) => ${output}", - "A custom function.", - ); - } CastInfo::Type(ty) => { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: (*ty).into(), - apply: Some(eco_format!("${{{ty}}}")), - detail: Some(eco_format!("A value of type {ty}.")), - }); - self.scope_completions(false, |value| value.type_name() == *ty); + if *ty == Type::of::<NoneValue>() { + self.snippet_completion("none", "none", "Nothing.") + } else if *ty == Type::of::<AutoValue>() { + self.snippet_completion("auto", "auto", "A smart default."); + } else if *ty == Type::of::<bool>() { + self.snippet_completion("false", "false", "No / Disabled."); + self.snippet_completion("true", "true", "Yes / Enabled."); + } else if *ty == Type::of::<Color>() { + self.snippet_completion( + "luma()", + "luma(${v})", + "A custom grayscale color.", + ); + self.snippet_completion( + "rgb()", + "rgb(${r}, ${g}, ${b}, ${a})", + "A custom RGBA color.", + ); + self.snippet_completion( + "cmyk()", + "cmyk(${c}, ${m}, ${y}, ${k})", + "A custom CMYK color.", + ); + self.scope_completions(false, |value| value.ty() == *ty); + } else if *ty == Type::of::<Func>() { + self.snippet_completion( + "function", + "(${params}) => ${output}", + "A custom function.", + ); + } else { + self.completions.push(Completion { + kind: CompletionKind::Syntax, + label: ty.long_name().into(), + apply: Some(eco_format!("${{{ty}}}")), + detail: Some(eco_format!("A value of type {ty}.")), + }); + self.scope_completions(false, |value| value.ty() == *ty); + } } CastInfo::Union(union) => { for info in union { diff --git a/crates/typst/src/ide/tooltip.rs b/crates/typst/src/ide/tooltip.rs index 4f2a345e..f310cad0 100644 --- a/crates/typst/src/ide/tooltip.rs +++ b/crates/typst/src/ide/tooltip.rs @@ -138,7 +138,7 @@ fn named_param_tooltip( world: &(dyn World + 'static), leaf: &LinkedNode, ) -> Option<Tooltip> { - let (info, named) = if_chain! { + let (func, named) = if_chain! { // Ensure that we are in a named pair in the arguments to a function // call or set rule. if let Some(parent) = leaf.parent(); @@ -155,8 +155,7 @@ fn named_param_tooltip( // Find metadata about the function. if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); - if let Some(info) = func.info(); - then { (info, named) } + then { (func, named) } else { return None; } }; @@ -164,7 +163,7 @@ fn named_param_tooltip( if_chain! { if leaf.index() == 0; if let Some(ident) = leaf.cast::<ast::Ident>(); - if let Some(param) = info.param(&ident); + if let Some(param) = func.param(&ident); then { return Some(Tooltip::Text(plain_docs_sentence(param.docs))); } @@ -173,8 +172,8 @@ fn named_param_tooltip( // Hovering over a string parameter value. if_chain! { if let Some(string) = leaf.cast::<ast::Str>(); - if let Some(param) = info.param(&named.name()); - if let Some(docs) = find_string_doc(¶m.cast, &string.get()); + if let Some(param) = func.param(&named.name()); + if let Some(docs) = find_string_doc(¶m.input, &string.get()); then { return Some(Tooltip::Text(docs.into())); } diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs index 3c2cae88..0ede1d7e 100644 --- a/crates/typst/src/image.rs +++ b/crates/typst/src/image.rs @@ -234,7 +234,7 @@ impl From<ttf_parser::RasterImageFormat> for ImageFormat { pub enum DecodedImage { /// A decoded pixel raster with its ICC profile. Raster(image::DynamicImage, Option<IccProfile>, RasterFormat), - /// An decoded SVG tree. + /// A decoded SVG tree. Svg(usvg::Tree), } diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 75ebe5be..608abbcc 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -104,7 +104,7 @@ pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Document> /// change and can thus even be cached across multiple compilations (for /// long-running applications like `typst watch`). Source files on the other /// hand can change and should thus be cleared after. Advanced clients like -/// language servers can also retain the source files and [edited](Source::edit) +/// language servers can also retain the source files and [edit](Source::edit) /// them in-place to benefit from better incremental performance. #[comemo::track] pub trait World { diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/model/content.rs index 6541ab6b..3a6c648c 100644 --- a/crates/typst/src/model/content.rs +++ b/crates/typst/src/model/content.rs @@ -8,20 +8,63 @@ use ecow::{eco_format, EcoString, EcoVec}; use serde::{Serialize, Serializer}; use super::{ - element, Behave, Behaviour, ElemFunc, Element, Guard, Label, Locatable, Location, + elem, Behave, Behaviour, Element, Guard, Label, Locatable, Location, NativeElement, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; -use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm}; +use crate::eval::{func, scope, ty, Dict, FromValue, IntoValue, Str, Value, Vm}; use crate::syntax::Span; use crate::util::pretty_array_like; -/// Composable representation of styled content. +/// A piece of document content. +/// +/// This type is at the heart of Typst. All markup you write and most +/// [functions]($function) you call produce content values. You can create a +/// content value by enclosing markup in square brackets. This is also how you +/// pass content to functions. +/// +/// # Example +/// ```example +/// Type of *Hello!* is +/// #type([*Hello!*]) +/// ``` +/// +/// Content can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. Wherever +/// content is expected, you can also pass a [string]($str) or `{none}`. +/// +/// # Representation +/// Content consists of elements with fields. When constructing an element with +/// its _element function,_ you provide these fields as arguments and when you +/// have a content value, you can access its fields with [field access +/// syntax]($scripting/#field-access). +/// +/// Some fields are required: These must be provided when constructing an +/// element and as a consequence, they are always available through field access +/// on content of that type. Required fields are marked as such in the +/// documentation. +/// +/// Most fields are optional: Like required fields, they can be passed to the +/// element function to configure them for a single element. However, these can +/// also be configured with [set rules]($styling/#set-rules) to apply them to +/// all elements within a scope. Optional fields are only available with field +/// access syntax when they are were explicitly passed to the element function, +/// not when they result from a set rule. +/// +/// Each element has a default appearance. However, you can also completely +/// customize its appearance with a [show rule]($styling/#show-rules). The show +/// rule is passed the element. It can access the element's field and produce +/// arbitrary content from it. +/// +/// In the web app, you can hover over a content variable to see exactly which +/// elements the content is composed of and what fields they have. +/// Alternatively, you can inspect the output of the [`repr`]($repr) function. +#[ty(scope)] #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] pub struct Content { - func: ElemFunc, + elem: Element, attrs: EcoVec<Attr>, } @@ -40,13 +83,13 @@ enum Attr { impl Content { /// Create an empty element. - pub fn new(func: ElemFunc) -> Self { - Self { func, attrs: EcoVec::new() } + pub fn new(elem: Element) -> Self { + Self { elem, attrs: EcoVec::new() } } /// Create empty content. pub fn empty() -> Self { - Self::new(SequenceElem::func()) + Self::new(SequenceElem::elem()) } /// Create a new sequence element from multiples elements. @@ -63,23 +106,18 @@ impl Content { content } - /// The element function of the contained content. - pub fn func(&self) -> ElemFunc { - self.func - } - /// Whether the content is an empty sequence. pub fn is_empty(&self) -> bool { self.is::<SequenceElem>() && self.attrs.is_empty() } /// Whether the contained element is of type `T`. - pub fn is<T: Element>(&self) -> bool { - self.func == T::func() + pub fn is<T: NativeElement>(&self) -> bool { + self.elem == T::elem() } /// Cast to `T` if the contained element is of type `T`. - pub fn to<T: Element>(&self) -> Option<&T> { + pub fn to<T: NativeElement>(&self) -> Option<&T> { T::unpack(self) } @@ -119,13 +157,13 @@ impl Content { where C: ?Sized + 'static, { - (self.func.0.vtable)(TypeId::of::<C>()).is_some() + self.elem.can::<C>() } - /// Whether the contained element has the given capability. - /// Where the capability is given by a `TypeId`. + /// Whether the contained element has the given capability where the + /// capability is given by a `TypeId`. pub fn can_type_id(&self, type_id: TypeId) -> bool { - (self.func.0.vtable)(type_id).is_some() + self.elem.can_type_id(type_id) } /// Cast to a trait object if the contained element has the given @@ -134,7 +172,7 @@ impl Content { where C: ?Sized + 'static, { - let vtable = (self.func.0.vtable)(TypeId::of::<C>())?; + let vtable = self.elem.vtable()(TypeId::of::<C>())?; let data = self as *const Self as *const (); Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) } @@ -145,7 +183,7 @@ impl Content { where C: ?Sized + 'static, { - let vtable = (self.func.0.vtable)(TypeId::of::<C>())?; + let vtable = self.elem.vtable()(TypeId::of::<C>())?; let data = self as *mut Self as *mut (); Some(unsafe { &mut *crate::util::fat::from_raw_parts_mut(data, vtable) }) } @@ -211,26 +249,6 @@ impl Content { /// Iter over all fields on the content. /// /// Does not include synthesized fields for sequence and styled elements. - pub fn fields(&self) -> impl Iterator<Item = (&EcoString, Value)> { - static CHILD: EcoString = EcoString::inline("child"); - static CHILDREN: EcoString = EcoString::inline("children"); - - let option = if let Some(iter) = self.to_sequence() { - Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect()))) - } else if let Some((child, _)) = self.to_styled() { - Some((&CHILD, Value::Content(child.clone()))) - } else { - None - }; - - self.fields_ref() - .map(|(name, value)| (name, value.clone())) - .chain(option) - } - - /// Iter over all fields on the content. - /// - /// Does not include synthesized fields for sequence and styled elements. pub fn fields_ref(&self) -> impl Iterator<Item = (&EcoString, &Value)> { let mut iter = self.attrs.iter(); std::iter::from_fn(move || { @@ -240,6 +258,11 @@ impl Content { }) } + /// Borrow the value of the given field. + pub fn get(&self, key: &str) -> StrResult<Value> { + self.field(key).ok_or_else(|| missing_field(key)) + } + /// Try to access a field on the content as a specified type. pub fn cast_field<T: FromValue>(&self, name: &str) -> Option<T> { match self.field(name) { @@ -254,25 +277,6 @@ impl Content { self.field(name).unwrap().cast().unwrap() } - /// Whether the content has the specified field. - pub fn has(&self, field: &str) -> bool { - self.field(field).is_some() - } - - /// Borrow the value of the given field. - pub fn at(&self, field: &str, default: Option<Value>) -> StrResult<Value> { - self.field(field) - .or(default) - .ok_or_else(|| missing_field_no_default(field)) - } - - /// Return the fields of the content as a dict. - pub fn dict(&self) -> Dict { - self.fields() - .map(|(key, value)| (key.to_owned().into(), value)) - .collect() - } - /// The content's label. pub fn label(&self) -> Option<&Label> { match self.field_ref("label")? { @@ -310,7 +314,7 @@ impl Content { prev.apply(styles); self } else { - let mut content = Content::new(StyledElem::func()); + let mut content = Content::new(StyledElem::elem()); content.attrs.push(Attr::Child(Prehashed::new(self))); content.attrs.push(Attr::Styles(styles)); content @@ -365,14 +369,6 @@ impl Content { && !self.is_prepared() } - /// This content's location in the document flow. - pub fn location(&self) -> Option<Location> { - self.attrs.iter().find_map(|modifier| match modifier { - Attr::Location(location) => Some(*location), - _ => None, - }) - } - /// Attach a location to this content. pub fn set_location(&mut self, location: Location) { self.attrs.push(Attr::Location(location)); @@ -451,9 +447,90 @@ impl Content { } } +#[scope] +impl Content { + /// The content's element function. This function can be used to create the element + /// contained in this content. It can be used in set and show rules for the + /// element. Can be compared with global functions to check whether you have + /// a specific + /// kind of element. + #[func] + pub fn func(&self) -> Element { + self.elem + } + + /// Whether the content has the specified field. + #[func] + pub fn has( + &self, + /// The field to look for. + field: Str, + ) -> bool { + self.field(&field).is_some() + } + + /// Access the specified field on the content. Returns the default value if + /// the field does not exist or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The field to access. + field: Str, + /// A default value to return if the field does not exist. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.field(&field) + .or(default) + .ok_or_else(|| missing_field_no_default(&field)) + } + + /// Returns the fields of this content. + /// + /// ```example + /// #rect( + /// width: 10cm, + /// height: 10cm, + /// ).fields() + /// ``` + #[func] + pub fn fields(&self) -> Dict { + static CHILD: EcoString = EcoString::inline("child"); + static CHILDREN: EcoString = EcoString::inline("children"); + + let option = if let Some(iter) = self.to_sequence() { + Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect()))) + } else if let Some((child, _)) = self.to_styled() { + Some((&CHILD, Value::Content(child.clone()))) + } else { + None + }; + + self.fields_ref() + .map(|(name, value)| (name, value.clone())) + .chain(option) + .map(|(key, value)| (key.to_owned().into(), value)) + .collect() + } + + /// The location of the content. This is only available on content returned + /// by [query]($query) or provided by a + /// [show rule]($reference/styling/#show-rules), for other content it will + /// be `{none}`. The resulting location can be used with + /// [counters]($counter), [state]($state) and [queries]($query). + #[func] + pub fn location(&self) -> Option<Location> { + self.attrs.iter().find_map(|modifier| match modifier { + Attr::Location(location) => Some(*location), + _ => None, + }) + } +} + impl Debug for Content { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let name = self.func.name(); + let name = self.elem.name(); if let Some(text) = item!(text_str)(self) { f.write_char('[')?; f.write_str(&text)?; @@ -465,6 +542,7 @@ impl Debug for Content { let mut pieces: Vec<_> = self .fields() + .into_iter() .map(|(name, value)| eco_format!("{name}: {value:?}")) .collect(); @@ -490,7 +568,7 @@ impl PartialEq for Content { } else if let (Some(left), Some(right)) = (self.to_styled(), other.to_styled()) { left == right } else { - self.func == other.func && self.fields_ref().eq(other.fields_ref()) + self.elem == other.elem && self.fields_ref().eq(other.fields_ref()) } } } @@ -536,8 +614,8 @@ impl Serialize for Content { S: Serializer, { serializer.collect_map( - iter::once((&"func".into(), self.func().name().into_value())) - .chain(self.fields()), + iter::once((&"func".into(), &self.func().name().into_value())) + .chain(self.fields_ref()), ) } } @@ -586,21 +664,16 @@ impl Attr { } } -/// Display: Sequence -/// Category: special -#[element] +/// Defines the `ElemFunc` for sequences. +#[elem] struct SequenceElem {} -/// Display: Sequence -/// Category: special -#[element] +/// Defines the `ElemFunc` for styled elements. +#[elem] struct StyledElem {} /// Hosts metadata and ensures metadata is produced even for empty elements. -/// -/// Display: Meta -/// Category: special -#[element(Behave)] +#[elem(Behave)] pub struct MetaElem { /// Metadata that should be attached to all elements affected by this style /// property. @@ -620,6 +693,12 @@ pub trait PlainText { fn plain_text(&self, text: &mut EcoString); } +/// The missing field access error message. +#[cold] +fn missing_field(field: &str) -> EcoString { + eco_format!("content does not contain field {:?}", Str::from(field)) +} + /// The missing field access error message when no default value was given. #[cold] fn missing_field_no_default(field: &str) -> EcoString { diff --git a/crates/typst/src/model/element.rs b/crates/typst/src/model/element.rs index 27010cd0..c9744cda 100644 --- a/crates/typst/src/model/element.rs +++ b/crates/typst/src/model/element.rs @@ -1,134 +1,174 @@ use std::any::TypeId; +use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use once_cell::sync::Lazy; use super::{Content, Selector, Styles}; use crate::diag::SourceResult; -use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm}; +use crate::eval::{cast, Args, Dict, Func, ParamInfo, Scope, Value, Vm}; +use crate::util::Static; /// A document element. -pub trait Element: Construct + Set + Sized + 'static { - /// Pack the element into type-erased content. - fn pack(self) -> Content; - - /// Extract this element from type-erased content. - fn unpack(content: &Content) -> Option<&Self>; +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Element(Static<NativeElementData>); - /// The element's function. - fn func() -> ElemFunc; -} - -/// An element's constructor function. -pub trait Construct { - /// Construct an element from the arguments. - /// - /// This is passed only the arguments that remain after execution of the - /// element's set rule. - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>; -} - -/// An element's set rule. -pub trait Set { - /// Parse relevant arguments into style properties for this element. - fn set(vm: &mut Vm, args: &mut Args) -> SourceResult<Styles>; -} - -/// An element's function. -#[derive(Copy, Clone)] -pub struct ElemFunc(pub(super) &'static NativeElemFunc); +impl Element { + /// Get the element for `T`. + pub fn of<T: NativeElement>() -> Self { + T::elem() + } -impl ElemFunc { - /// The function's name. + /// The element's normal name (e.g. `enum`). pub fn name(self) -> &'static str { self.0.name } - /// Apply the given arguments to the function. - pub fn with(self, args: Args) -> Func { - Func::from(self).with(args) + /// The element's title case name, for use in documentation + /// (e.g. `Numbered List`). + pub fn title(&self) -> &'static str { + self.0.title } - /// Extract details about the function. - pub fn info(&self) -> &'static FuncInfo { - &self.0.info + /// Documentation for the element (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs } - /// Construct an element. + /// Search keywords for the element. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// Construct an instance of this element. pub fn construct(self, vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { (self.0.construct)(vm, args) } - /// Whether the contained element has the given capability. - pub fn can<C>(&self) -> bool + /// Execute the set rule for the element and return the resulting style map. + pub fn set(self, vm: &mut Vm, mut args: Args) -> SourceResult<Styles> { + let styles = (self.0.set)(vm, &mut args)?; + args.finish()?; + Ok(styles) + } + + /// Whether the element has the given capability. + pub fn can<C>(self) -> bool where C: ?Sized + 'static, { - (self.0.vtable)(TypeId::of::<C>()).is_some() + self.can_type_id(TypeId::of::<C>()) } - /// Create a selector for elements of this function. + /// Whether the element has the given capability where the capability is + /// given by a `TypeId`. + pub fn can_type_id(self, type_id: TypeId) -> bool { + (self.0.vtable)(type_id).is_some() + } + + /// The VTable for capabilities dispatch. + pub fn vtable(self) -> fn(of: TypeId) -> Option<*const ()> { + self.0.vtable + } + + /// Create a selector for this element. pub fn select(self) -> Selector { Selector::Elem(self, None) } - /// Create a selector for elements of this function, filtering for those - /// whose [fields](super::Content::field) match the given arguments. + /// Create a selector for this element, filtering for those + /// that [fields](super::Content::field) match the given argument. pub fn where_(self, fields: Dict) -> Selector { Selector::Elem(self, Some(fields)) } - /// Execute the set rule for the element and return the resulting style map. - pub fn set(self, vm: &mut Vm, mut args: Args) -> SourceResult<Styles> { - let styles = (self.0.set)(vm, &mut args)?; - args.finish()?; - Ok(styles) + /// The element's associated scope of sub-definition. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Details about the element's fields. + pub fn params(&self) -> &'static [ParamInfo] { + &(self.0).0.params } } -impl Debug for ElemFunc { +impl Debug for Element { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(self.name()) } } -impl Eq for ElemFunc {} - -impl PartialEq for ElemFunc { - fn eq(&self, other: &Self) -> bool { - std::ptr::eq(self.0, other.0) +impl Ord for Element { + fn cmp(&self, other: &Self) -> Ordering { + self.name().cmp(other.name()) } } -impl Hash for ElemFunc { - fn hash<H: Hasher>(&self, state: &mut H) { - state.write_usize(self.0 as *const _ as usize); +impl PartialOrd for Element { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) } } cast! { - ElemFunc, + Element, self => Value::Func(self.into()), - v: Func => v.element().ok_or("expected element function")?, + v: Func => v.element().ok_or("expected element")?, } -impl From<&'static NativeElemFunc> for ElemFunc { - fn from(native: &'static NativeElemFunc) -> Self { - Self(native) +/// A Typst element that is defined by a native Rust type. +pub trait NativeElement: Construct + Set + Sized + 'static { + /// Get the element for the native Rust element. + fn elem() -> Element { + Element::from(Self::data()) } + + /// Get the element data for the native Rust element. + fn data() -> &'static NativeElementData; + + /// Pack the element into type-erased content. + fn pack(self) -> Content; + + /// Extract this element from type-erased content. + fn unpack(content: &Content) -> Option<&Self>; } -/// An element function backed by a Rust type. -pub struct NativeElemFunc { - /// The element's name. +/// An element's constructor function. +pub trait Construct { + /// Construct an element from the arguments. + /// + /// This is passed only the arguments that remain after execution of the + /// element's set rule. + fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>; +} + +/// An element's set rule. +pub trait Set { + /// Parse relevant arguments into style properties for this element. + fn set(vm: &mut Vm, args: &mut Args) -> SourceResult<Styles>; +} + +/// Defines a native element. +pub struct NativeElementData { pub name: &'static str, - /// The element's vtable for capability dispatch. - pub vtable: fn(of: TypeId) -> Option<*const ()>, - /// The element's constructor. + pub title: &'static str, + pub docs: &'static str, + pub keywords: &'static [&'static str], pub construct: fn(&mut Vm, &mut Args) -> SourceResult<Content>, - /// The element's set rule. pub set: fn(&mut Vm, &mut Args) -> SourceResult<Styles>, - /// Details about the function. - pub info: Lazy<FuncInfo>, + pub vtable: fn(of: TypeId) -> Option<*const ()>, + pub scope: Lazy<Scope>, + pub params: Lazy<Vec<ParamInfo>>, +} + +impl From<&'static NativeElementData> for Element { + fn from(data: &'static NativeElementData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeElementData, + self => Element::from(self).into_value(), } diff --git a/crates/typst/src/model/introspect.rs b/crates/typst/src/model/introspect.rs index 2b2693d9..90598679 100644 --- a/crates/typst/src/model/introspect.rs +++ b/crates/typst/src/model/introspect.rs @@ -11,14 +11,19 @@ use indexmap::IndexMap; use super::{Content, Selector}; use crate::diag::{bail, StrResult}; use crate::doc::{Frame, FrameItem, Meta, Position}; -use crate::eval::{cast, Value}; +use crate::eval::{cast, func, scope, ty, Dict, Value, Vm}; use crate::geom::{Point, Transform}; use crate::model::Label; use crate::util::NonZeroExt; -/// Identifies the location of an element in the document. +/// Identifies an element in the document. /// -/// This struct is created by [`Locator::locate`]. +/// A location uniquely identifies an element in the document and lets you +/// access its absolute position on the pages. You can retrieve the current +/// location with the [`locate`]($locate) function and the location of a queried +/// or shown element with the [`location()`]($content.location) method on +/// content. +#[ty(scope)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Location { /// The hash of the element. @@ -40,6 +45,44 @@ impl Location { } } +#[scope] +impl Location { + /// Return the page number for this location. + /// + /// Note that this does not return the value of the [page counter]($counter) + /// at this location, but the true page number (starting from one). + /// + /// If you want to know the value of the page counter, use + /// `{counter(page).at(loc)}` instead. + #[func] + pub fn page(self, vm: &mut Vm) -> NonZeroUsize { + vm.vt.introspector.page(self) + } + + /// Return a dictionary with the page number and the x, y position for this + /// location. The page number starts at one and the coordinates are measured + /// from the top-left of the page. + /// + /// If you only need the page number, use `page()` instead as it allows + /// Typst to skip unnecessary work. + #[func] + pub fn position(self, vm: &mut Vm) -> Dict { + vm.vt.introspector.position(self).into() + } + + /// Returns the page numbering pattern of the page at this location. This + /// can be used when displaying the page counter in order to obtain the + /// local numbering. This is useful if you are building custom indices or + /// outlines. + /// + /// If the page numbering is set to `none` at that location, this function + /// returns `none`. + #[func] + pub fn page_numbering(self, vm: &mut Vm) -> Value { + vm.vt.introspector.page_numbering(self) + } +} + impl Debug for Location { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad("..") @@ -47,7 +90,7 @@ impl Debug for Location { } cast! { - type Location: "location", + type Location, } /// Provides locations for elements in the document. diff --git a/crates/typst/src/model/label.rs b/crates/typst/src/model/label.rs index ef8f3edd..79f73881 100644 --- a/crates/typst/src/model/label.rs +++ b/crates/typst/src/model/label.rs @@ -2,10 +2,42 @@ use std::fmt::{self, Debug, Formatter}; use ecow::EcoString; +use crate::eval::{func, scope, ty}; + /// A label for an element. +/// +/// Inserting a label into content attaches it to the closest previous element +/// that is not a space. Then, the element can be [referenced]($ref) and styled +/// through the label. +/// +/// # Example +/// ```example +/// #show <a>: set text(blue) +/// #show label("b"): set text(red) +/// +/// = Heading <a> +/// *Strong* #label("b") +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: You can create a label by enclosing +/// its name in angle brackets. This works both in markup and code. +#[ty(scope)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Label(pub EcoString); +#[scope] +impl Label { + /// Creates a label from a string. + #[func(constructor)] + pub fn construct( + /// The name of the label. + name: EcoString, + ) -> Label { + Self(name) + } +} + impl Debug for Label { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "<{}>", self.0) diff --git a/crates/typst/src/model/mod.rs b/crates/typst/src/model/mod.rs index 100006b4..2741f731 100644 --- a/crates/typst/src/model/mod.rs +++ b/crates/typst/src/model/mod.rs @@ -9,10 +9,10 @@ mod selector; mod styles; #[doc(inline)] -pub use typst_macros::element; +pub use typst_macros::elem; pub use self::content::{Content, MetaElem, PlainText}; -pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set}; +pub use self::element::{Construct, Element, NativeElement, NativeElementData, Set}; pub use self::introspect::{Introspector, Location, Locator}; pub use self::label::{Label, Unlabellable}; pub use self::realize::{ diff --git a/crates/typst/src/model/realize.rs b/crates/typst/src/model/realize.rs index 3e683d3e..745d7f43 100644 --- a/crates/typst/src/model/realize.rs +++ b/crates/typst/src/model/realize.rs @@ -1,4 +1,6 @@ -use super::{Content, ElemFunc, Element, MetaElem, Recipe, Selector, StyleChain, Vt}; +use super::{ + Content, Element, MetaElem, NativeElement, Recipe, Selector, StyleChain, Vt, +}; use crate::diag::SourceResult; use crate::doc::Meta; use crate::util::hash128; @@ -226,5 +228,5 @@ pub enum Guard { /// The nth recipe from the top of the chain. Nth(usize), /// The [base recipe](Show) for a kind of element. - Base(ElemFunc), + Base(Element), } diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/model/selector.rs index 5e4f257b..9264c9ea 100644 --- a/crates/typst/src/model/selector.rs +++ b/crates/typst/src/model/selector.rs @@ -4,22 +4,58 @@ use std::sync::Arc; use ecow::{eco_format, EcoString, EcoVec}; -use super::{Content, ElemFunc, Label, Location}; +use super::{Content, Element, Label, Locatable, Location}; use crate::diag::{bail, StrResult}; use crate::eval::{ - cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value, + cast, func, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex, Str, Symbol, + Type, Value, }; -use crate::model::Locatable; use crate::util::pretty_array_like; -/// A selector in a show rule. +/// A filter for selecting elements within the document. +/// +/// You can construct a selector in the following ways: +/// - you can use an element [function]($function) +/// - you can filter for an element function with +/// [specific fields]($function.where) +/// - you can use a [string]($str) or [regular expression]($regex) +/// - you can use a [`{<label>}`]($label) +/// - you can use a [`location`]($location) +/// - call the [`selector`]($selector) constructor to convert any of the above +/// types into a selector value and use the methods below to refine it +/// +/// Selectors are used to [apply styling rules]($styling/#show-rules) to +/// elements. You can also use selectors to [query]($query) the document for +/// certain types of elements. +/// +/// Furthermore, you can pass a selector to several of Typst's built-in +/// functions to configure their behaviour. One such example is the +/// [outline]($outline) where it can be used to change which elements are listed +/// within the outline. +/// +/// Multiple selectors can be combined using the methods shown below. However, +/// not all kinds of selectors are supported in all places, at the moment. +/// +/// # Example +/// ```example +/// #locate(loc => query( +/// heading.where(level: 1) +/// .or(heading.where(level: 2)), +/// loc, +/// )) +/// +/// = This will be found +/// == So will this +/// === But this will not. +/// ``` +#[ty(scope)] #[derive(Clone, PartialEq, Hash)] pub enum Selector { /// Matches a specific type of element. /// /// If there is a dictionary, only elements with the fields from the /// dictionary match. - Elem(ElemFunc, Option<Dict>), + Elem(Element, Option<Dict>), /// Matches the element at the specified location. Location(Location), /// Matches elements with a specific label. @@ -63,36 +99,6 @@ impl Selector { Self::Can(TypeId::of::<T>()) } - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::And`] selector. - pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::And(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::Or`] selector. - pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::Or(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector into a [`Selector::Before`] selector. - pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::Before { - selector: Arc::new(self), - end: Arc::new(location.into()), - inclusive, - } - } - - /// Transforms this selector into a [`Selector::After`] selector. - pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::After { - selector: Arc::new(self), - start: Arc::new(location.into()), - inclusive, - } - } - /// Whether the selector matches for the target. pub fn matches(&self, target: &Content) -> bool { match self { @@ -105,7 +111,7 @@ impl Selector { } Self::Label(label) => target.label() == Some(label), Self::Regex(regex) => { - target.func() == item!(text_func) + target.func() == item!(text_elem) && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) } Self::Can(cap) => target.can_type_id(*cap), @@ -118,6 +124,85 @@ impl Selector { } } +#[scope] +impl Selector { + /// Turns a value into a selector. The following values are accepted: + /// - An element function like a `heading` or `figure`. + /// - A `{<label>}`. + /// - A more complex selector like `{heading.where(level: 1)}`. + #[func(constructor)] + pub fn construct( + /// Can be an element function like a `heading` or `figure`, a `{<label>}` + /// or a more complex selector like `{heading.where(level: 1)}`. + target: Selector, + ) -> Selector { + target + } + + /// Selects all elements that match this or any of the other selectors. + #[func] + pub fn or( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<LocatableSelector>, + ) -> Selector { + Self::Or(others.into_iter().map(|s| s.0).chain(Some(self)).collect()) + } + + /// Selects all elements that match this and all of the the other selectors. + #[func] + pub fn and( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<LocatableSelector>, + ) -> Selector { + Self::And(others.into_iter().map(|s| s.0).chain(Some(self)).collect()) + } + + /// Returns a modified selector that will only match elements that occur + /// before the first match of `end`. + #[func] + pub fn before( + self, + /// The original selection will end at the first match of `end`. + end: LocatableSelector, + /// Whether `end` itself should match or not. This is only relevant if + /// both selectors match the same type of element. Defaults to `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::Before { + selector: Arc::new(self), + end: Arc::new(end.0), + inclusive, + } + } + + /// Returns a modified selector that will only match elements that occur + /// after the first match of `start`. + #[func] + pub fn after( + self, + /// The original selection will start at the first match of `start`. + start: LocatableSelector, + /// Whether `start` itself should match or not. This is only relevant + /// if both selectors match the same type of element. Defaults to + /// `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::After { + selector: Arc::new(self), + start: Arc::new(start.0), + inclusive, + } + } +} + impl From<Location> for Selector { fn from(value: Location) -> Self { Self::Location(value) @@ -166,7 +251,7 @@ impl Debug for Selector { } cast! { - type Selector: "selector", + type Selector, func: Func => func .element() .ok_or("only element functions can be used as selectors")? @@ -185,23 +270,26 @@ cast! { pub struct LocatableSelector(pub Selector); impl Reflect for LocatableSelector { - fn describe() -> CastInfo { + fn input() -> CastInfo { CastInfo::Union(vec![ - CastInfo::Type("function"), - CastInfo::Type("label"), - CastInfo::Type("selector"), + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Selector>()), ]) } + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + fn castable(value: &Value) -> bool { - matches!(value.type_name(), "function" | "label" | "selector") + Label::castable(value) || Func::castable(value) || Selector::castable(value) } } -impl IntoValue for LocatableSelector { - fn into_value(self) -> Value { - self.0.into_value() - } +cast! { + LocatableSelector, + self => self.0.into_value(), } impl FromValue for LocatableSelector { @@ -242,6 +330,12 @@ impl FromValue for LocatableSelector { } } +impl From<Location> for LocatableSelector { + fn from(loc: Location) -> Self { + Self(Selector::Location(loc)) + } +} + /// A selector that can be used with show rules. /// /// Hopefully, this is made obsolete by a more powerful showing mechanism in the @@ -250,34 +344,34 @@ impl FromValue for LocatableSelector { pub struct ShowableSelector(pub Selector); impl Reflect for ShowableSelector { - fn describe() -> CastInfo { + fn input() -> CastInfo { CastInfo::Union(vec![ - CastInfo::Type("function"), - CastInfo::Type("label"), - CastInfo::Type("string"), - CastInfo::Type("regular expression"), - CastInfo::Type("symbol"), - CastInfo::Type("selector"), + CastInfo::Type(Type::of::<Symbol>()), + CastInfo::Type(Type::of::<Str>()), + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Regex>()), + CastInfo::Type(Type::of::<Selector>()), ]) } + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + fn castable(value: &Value) -> bool { - matches!( - value.type_name(), - "symbol" - | "string" - | "label" - | "function" - | "regular expression" - | "selector" - ) + Symbol::castable(value) + || Str::castable(value) + || Label::castable(value) + || Func::castable(value) + || Regex::castable(value) + || Selector::castable(value) } } -impl IntoValue for ShowableSelector { - fn into_value(self) -> Value { - self.0.into_value() - } +cast! { + ShowableSelector, + self => self.0.into_value(), } impl FromValue for ShowableSelector { diff --git a/crates/typst/src/model/styles.rs b/crates/typst/src/model/styles.rs index 75172680..347eae4c 100644 --- a/crates/typst/src/model/styles.rs +++ b/crates/typst/src/model/styles.rs @@ -6,12 +6,13 @@ use std::ptr; use comemo::Prehashed; use ecow::{eco_vec, EcoString, EcoVec}; -use super::{Content, ElemFunc, Element, Selector, Vt}; +use super::{Content, Element, NativeElement, Selector, Vt}; use crate::diag::{SourceResult, Trace, Tracepoint}; -use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm}; +use crate::eval::{cast, ty, Args, FromValue, Func, IntoValue, Value, Vm}; use crate::syntax::Span; /// A list of style properties. +#[ty] #[derive(Default, PartialEq, Clone, Hash)] pub struct Styles(EcoVec<Prehashed<Style>>); @@ -70,11 +71,11 @@ impl Styles { /// Returns `Some(_)` with an optional span if this list contains /// styles for the given element. - pub fn interruption<T: Element>(&self) -> Option<Option<Span>> { - let func = T::func(); + pub fn interruption<T: NativeElement>(&self) -> Option<Option<Span>> { + let elem = T::elem(); self.0.iter().find_map(|entry| match &**entry { - Style::Property(property) => property.is_of(func).then_some(property.span), - Style::Recipe(recipe) => recipe.is_of(func).then_some(Some(recipe.span)), + Style::Property(property) => property.is_of(elem).then_some(property.span), + Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)), }) } } @@ -143,7 +144,7 @@ impl From<Recipe> for Style { #[derive(Clone, PartialEq, Hash)] pub struct Property { /// The element the property belongs to. - element: ElemFunc, + elem: Element, /// The property's name. name: EcoString, /// The property's value. @@ -154,13 +155,9 @@ pub struct Property { impl Property { /// Create a new property from a key-value pair. - pub fn new( - element: ElemFunc, - name: impl Into<EcoString>, - value: impl IntoValue, - ) -> Self { + pub fn new(elem: Element, name: impl Into<EcoString>, value: impl IntoValue) -> Self { Self { - element, + elem, name: name.into(), value: value.into_value(), span: None, @@ -168,19 +165,19 @@ impl Property { } /// Whether this property is the given one. - pub fn is(&self, element: ElemFunc, name: &str) -> bool { - self.element == element && self.name == name + pub fn is(&self, elem: Element, name: &str) -> bool { + self.elem == elem && self.name == name } /// Whether this property belongs to the given element. - pub fn is_of(&self, element: ElemFunc) -> bool { - self.element == element + pub fn is_of(&self, elem: Element) -> bool { + self.elem == elem } } impl Debug for Property { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "set {}({}: {:?})", self.element.name(), self.name, self.value)?; + write!(f, "set {}({}: {:?})", self.elem.name(), self.name, self.value)?; Ok(()) } } @@ -198,7 +195,7 @@ pub struct Recipe { impl Recipe { /// Whether this recipe is for the given type of element. - pub fn is_of(&self, element: ElemFunc) -> bool { + pub fn is_of(&self, element: Element) -> bool { match self.selector { Some(Selector::Elem(own, _)) => own == element, _ => false, @@ -323,7 +320,7 @@ impl<'a> StyleChain<'a> { /// Cast the first value for the given property in the chain. pub fn get<T: FromValue>( self, - func: ElemFunc, + func: Element, name: &'a str, inherent: Option<Value>, default: impl Fn() -> T, @@ -336,7 +333,7 @@ impl<'a> StyleChain<'a> { /// Cast the first value for the given property in the chain. pub fn get_resolve<T: FromValue + Resolve>( self, - func: ElemFunc, + func: Element, name: &'a str, inherent: Option<Value>, default: impl Fn() -> T, @@ -347,7 +344,7 @@ impl<'a> StyleChain<'a> { /// Cast the first value for the given property in the chain. pub fn get_fold<T: FromValue + Fold>( self, - func: ElemFunc, + func: Element, name: &'a str, inherent: Option<Value>, default: impl Fn() -> T::Output, @@ -368,7 +365,7 @@ impl<'a> StyleChain<'a> { /// Cast the first value for the given property in the chain. pub fn get_resolve_fold<T>( self, - func: ElemFunc, + func: Element, name: &'a str, inherent: Option<Value>, default: impl Fn() -> <T::Output as Fold>::Output, @@ -402,7 +399,7 @@ impl<'a> StyleChain<'a> { /// Iterate over all values for the given property in the chain. pub fn properties<T: FromValue + 'a>( self, - func: ElemFunc, + func: Element, name: &'a str, inherent: Option<Value>, ) -> impl Iterator<Item = T> + '_ { diff --git a/crates/typst/src/util/fmt.rs b/crates/typst/src/util/fmt.rs new file mode 100644 index 00000000..a2686dd7 --- /dev/null +++ b/crates/typst/src/util/fmt.rs @@ -0,0 +1,78 @@ +/// Format pieces separated with commas and a final "and" or "or". +pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String { + let mut buf = String::new(); + for (i, part) in pieces.iter().enumerate() { + match i { + 0 => {} + 1 if pieces.len() == 2 => { + buf.push(' '); + buf.push_str(last); + buf.push(' '); + } + i if i + 1 == pieces.len() => { + buf.push_str(", "); + buf.push_str(last); + buf.push(' '); + } + _ => buf.push_str(", "), + } + buf.push_str(part.as_ref()); + } + buf +} + +/// Format a comma-separated list. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String { + const MAX_WIDTH: usize = 50; + + let mut buf = String::new(); + let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>() + + 2 * pieces.len().saturating_sub(1); + + if len <= MAX_WIDTH { + for (i, piece) in pieces.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(piece.as_ref()); + } + if trailing_comma { + buf.push(','); + } + } else { + for piece in pieces { + buf.push_str(piece.as_ref().trim()); + buf.push_str(",\n"); + } + } + + buf +} + +/// Format an array-like construct. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String { + let list = pretty_comma_list(parts, trailing_comma); + let mut buf = String::new(); + buf.push('('); + if list.contains('\n') { + buf.push('\n'); + for (i, line) in list.lines().enumerate() { + if i > 0 { + buf.push('\n'); + } + buf.push_str(" "); + buf.push_str(line); + } + buf.push('\n'); + } else { + buf.push_str(&list); + } + buf.push(')'); + buf +} diff --git a/crates/typst/src/util/mod.rs b/crates/typst/src/util/mod.rs index 1ba85bbb..6c183a4f 100644 --- a/crates/typst/src/util/mod.rs +++ b/crates/typst/src/util/mod.rs @@ -1,10 +1,14 @@ //! Utilities. pub mod fat; +mod fmt; -use std::fmt::{self, Debug, Formatter}; +pub use self::fmt::{pretty_array_like, pretty_comma_list, separated_list}; + +use std::fmt::{Debug, Formatter}; use std::hash::Hash; use std::num::NonZeroUsize; +use std::ops::Deref; use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; @@ -12,15 +16,15 @@ use siphasher::sip128::{Hasher128, SipHasher13}; /// Turn a closure into a struct implementing [`Debug`]. pub fn debug<F>(f: F) -> impl Debug where - F: Fn(&mut Formatter) -> fmt::Result, + F: Fn(&mut Formatter) -> std::fmt::Result, { struct Wrapper<F>(F); impl<F> Debug for Wrapper<F> where - F: Fn(&mut Formatter) -> fmt::Result, + F: Fn(&mut Formatter) -> std::fmt::Result, { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0(f) } } @@ -103,89 +107,44 @@ where } } -/// Format pieces separated with commas and a final "and" or "or". -pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String { - let mut buf = String::new(); - for (i, part) in pieces.iter().enumerate() { - match i { - 0 => {} - 1 if pieces.len() == 2 => { - buf.push(' '); - buf.push_str(last); - buf.push(' '); - } - i if i + 1 == pieces.len() => { - buf.push_str(", "); - buf.push_str(last); - buf.push(' '); - } - _ => buf.push_str(", "), - } - buf.push_str(part.as_ref()); - } - buf +/// Check if the [`Option`]-wrapped L is same to R. +pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool +where + L: PartialEq<R>, +{ + left.map_or(false, |v| v == other) } -/// Format a comma-separated list. -/// -/// Tries to format horizontally, but falls back to vertical formatting if the -/// pieces are too long. -pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String { - const MAX_WIDTH: usize = 50; - - let mut buf = String::new(); - let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>() - + 2 * pieces.len().saturating_sub(1); - - if len <= MAX_WIDTH { - for (i, piece) in pieces.iter().enumerate() { - if i > 0 { - buf.push_str(", "); - } - buf.push_str(piece.as_ref()); - } - if trailing_comma { - buf.push(','); - } - } else { - for piece in pieces { - buf.push_str(piece.as_ref().trim()); - buf.push_str(",\n"); - } +/// A container around a static reference that is cheap to clone and hash. +#[derive(Debug)] +pub struct Static<T: 'static>(pub &'static T); + +impl<T> Deref for Static<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0 } +} + +impl<T> Copy for Static<T> {} - buf +impl<T> Clone for Static<T> { + fn clone(&self) -> Self { + *self + } } -/// Format an array-like construct. -/// -/// Tries to format horizontally, but falls back to vertical formatting if the -/// pieces are too long. -pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String { - let list = pretty_comma_list(parts, trailing_comma); - let mut buf = String::new(); - buf.push('('); - if list.contains('\n') { - buf.push('\n'); - for (i, line) in list.lines().enumerate() { - if i > 0 { - buf.push('\n'); - } - buf.push_str(" "); - buf.push_str(line); - } - buf.push('\n'); - } else { - buf.push_str(&list); +impl<T> Eq for Static<T> {} + +impl<T> PartialEq for Static<T> { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.0, other.0) } - buf.push(')'); - buf } -/// Check if the [`Option`]-wrapped L is same to R. -pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool -where - L: PartialEq<R>, -{ - left.map(|v| v == other).unwrap_or(false) +impl<T> Hash for Static<T> { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + state.write_usize(self.0 as *const _ as _); + } } |
