Fully untyped model

This commit is contained in:
Laurenz 2023-03-07 15:17:13 +01:00
parent 6ab7760822
commit 25b5bd1175
101 changed files with 4611 additions and 4922 deletions

View File

@ -1,3 +1,5 @@
//! Calculations and processing of numeric values.
use std::cmp::Ordering; use std::cmp::Ordering;
use std::ops::Rem; use std::ops::Rem;
@ -6,7 +8,7 @@ use typst::eval::{Module, Scope};
use crate::prelude::*; use crate::prelude::*;
/// A module with computational functions. /// A module with computational functions.
pub fn calc() -> Module { pub fn module() -> Module {
let mut scope = Scope::new(); let mut scope = Scope::new();
scope.def_func::<AbsFunc>("abs"); scope.def_func::<AbsFunc>("abs");
scope.def_func::<PowFunc>("pow"); scope.def_func::<PowFunc>("pow");
@ -37,7 +39,6 @@ pub fn calc() -> Module {
Module::new("calc").with_scope(scope) Module::new("calc").with_scope(scope)
} }
/// # Absolute
/// Calculate the absolute value of a numeric value. /// Calculate the absolute value of a numeric value.
/// ///
/// ## Example /// ## Example
@ -51,8 +52,8 @@ pub fn calc() -> Module {
/// - value: `ToAbs` (positional, required) /// - value: `ToAbs` (positional, required)
/// The value whose absolute value to calculate. /// The value whose absolute value to calculate.
/// ///
/// ## Category /// Display: Absolute
/// calculate /// Category: calculate
#[func] #[func]
pub fn abs(args: &mut Args) -> SourceResult<Value> { pub fn abs(args: &mut Args) -> SourceResult<Value> {
Ok(args.expect::<ToAbs>("value")?.0) Ok(args.expect::<ToAbs>("value")?.0)
@ -61,7 +62,7 @@ pub fn abs(args: &mut Args) -> SourceResult<Value> {
/// A value of which the absolute value can be taken. /// A value of which the absolute value can be taken.
struct ToAbs(Value); struct ToAbs(Value);
castable! { cast_from_value! {
ToAbs, ToAbs,
v: i64 => Self(Value::Int(v.abs())), v: i64 => Self(Value::Int(v.abs())),
v: f64 => Self(Value::Float(v.abs())), v: f64 => Self(Value::Float(v.abs())),
@ -72,7 +73,6 @@ castable! {
v: Fr => Self(Value::Fraction(v.abs())), v: Fr => Self(Value::Fraction(v.abs())),
} }
/// # Power
/// Raise a value to some exponent. /// Raise a value to some exponent.
/// ///
/// ## Example /// ## Example
@ -86,8 +86,8 @@ castable! {
/// - exponent: `Num` (positional, required) /// - exponent: `Num` (positional, required)
/// The exponent of the power. Must be non-negative. /// The exponent of the power. Must be non-negative.
/// ///
/// ## Category /// Display: Power
/// calculate /// Category: calculate
#[func] #[func]
pub fn pow(args: &mut Args) -> SourceResult<Value> { pub fn pow(args: &mut Args) -> SourceResult<Value> {
let base = args.expect::<Num>("base")?; let base = args.expect::<Num>("base")?;
@ -103,7 +103,6 @@ pub fn pow(args: &mut Args) -> SourceResult<Value> {
Ok(base.apply2(exponent, |a, b| a.pow(b as u32), f64::powf)) Ok(base.apply2(exponent, |a, b| a.pow(b as u32), f64::powf))
} }
/// # Square Root
/// Calculate the square root of a number. /// Calculate the square root of a number.
/// ///
/// ## Example /// ## Example
@ -116,8 +115,8 @@ pub fn pow(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number whose square root to calculate. Must be non-negative. /// The number whose square root to calculate. Must be non-negative.
/// ///
/// ## Category /// Display: Square Root
/// calculate /// Category: calculate
#[func] #[func]
pub fn sqrt(args: &mut Args) -> SourceResult<Value> { pub fn sqrt(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Spanned<Num>>("value")?; let value = args.expect::<Spanned<Num>>("value")?;
@ -127,7 +126,6 @@ pub fn sqrt(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Float(value.v.float().sqrt())) Ok(Value::Float(value.v.float().sqrt()))
} }
/// # Sine
/// Calculate the sine of an angle. /// Calculate the sine of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as /// When called with an integer or a float, they will be interpreted as
@ -144,8 +142,8 @@ pub fn sqrt(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose sine to calculate. /// The angle whose sine to calculate.
/// ///
/// ## Category /// Display: Sine
/// calculate /// Category: calculate
#[func] #[func]
pub fn sin(args: &mut Args) -> SourceResult<Value> { pub fn sin(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -156,7 +154,6 @@ pub fn sin(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Cosine
/// Calculate the cosine of an angle. /// Calculate the cosine of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as /// When called with an integer or a float, they will be interpreted as
@ -173,8 +170,8 @@ pub fn sin(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose cosine to calculate. /// The angle whose cosine to calculate.
/// ///
/// ## Category /// Display: Cosine
/// calculate /// Category: calculate
#[func] #[func]
pub fn cos(args: &mut Args) -> SourceResult<Value> { pub fn cos(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -185,7 +182,6 @@ pub fn cos(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Tangent
/// Calculate the tangent of an angle. /// Calculate the tangent of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as /// When called with an integer or a float, they will be interpreted as
@ -201,8 +197,8 @@ pub fn cos(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose tangent to calculate. /// The angle whose tangent to calculate.
/// ///
/// ## Category /// Display: Tangent
/// calculate /// Category: calculate
#[func] #[func]
pub fn tan(args: &mut Args) -> SourceResult<Value> { pub fn tan(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -213,7 +209,6 @@ pub fn tan(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Arcsine
/// Calculate the arcsine of a number. /// Calculate the arcsine of a number.
/// ///
/// ## Example /// ## Example
@ -226,8 +221,8 @@ pub fn tan(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number whose arcsine to calculate. Must be between -1 and 1. /// The number whose arcsine to calculate. Must be between -1 and 1.
/// ///
/// ## Category /// Display: Arcsine
/// calculate /// Category: calculate
#[func] #[func]
pub fn asin(args: &mut Args) -> SourceResult<Value> { pub fn asin(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?; let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?;
@ -238,7 +233,6 @@ pub fn asin(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Angle(Angle::rad(val.asin()))) Ok(Value::Angle(Angle::rad(val.asin())))
} }
/// # Arccosine
/// Calculate the arccosine of a number. /// Calculate the arccosine of a number.
/// ///
/// ## Example /// ## Example
@ -251,8 +245,8 @@ pub fn asin(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number whose arccosine to calculate. Must be between -1 and 1. /// The number whose arccosine to calculate. Must be between -1 and 1.
/// ///
/// ## Category /// Display: Arccosine
/// calculate /// Category: calculate
#[func] #[func]
pub fn acos(args: &mut Args) -> SourceResult<Value> { pub fn acos(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?; let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?;
@ -263,7 +257,6 @@ pub fn acos(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Angle(Angle::rad(val.acos()))) Ok(Value::Angle(Angle::rad(val.acos())))
} }
/// # Arctangent
/// Calculate the arctangent of a number. /// Calculate the arctangent of a number.
/// ///
/// ## Example /// ## Example
@ -276,15 +269,14 @@ pub fn acos(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number whose arctangent to calculate. /// The number whose arctangent to calculate.
/// ///
/// ## Category /// Display: Arctangent
/// calculate /// Category: calculate
#[func] #[func]
pub fn atan(args: &mut Args) -> SourceResult<Value> { pub fn atan(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Num>("value")?; let value = args.expect::<Num>("value")?;
Ok(Value::Angle(Angle::rad(value.float().atan()))) Ok(Value::Angle(Angle::rad(value.float().atan())))
} }
/// # Hyperbolic sine
/// Calculate the hyperbolic sine of an angle. /// Calculate the hyperbolic sine of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as radians. /// When called with an integer or a float, they will be interpreted as radians.
@ -299,8 +291,8 @@ pub fn atan(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose hyperbolic sine to calculate. /// The angle whose hyperbolic sine to calculate.
/// ///
/// ## Category /// Display: Hyperbolic sine
/// calculate /// Category: calculate
#[func] #[func]
pub fn sinh(args: &mut Args) -> SourceResult<Value> { pub fn sinh(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -311,7 +303,6 @@ pub fn sinh(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Hyperbolic cosine
/// Calculate the hyperbolic cosine of an angle. /// Calculate the hyperbolic cosine of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as radians. /// When called with an integer or a float, they will be interpreted as radians.
@ -326,8 +317,8 @@ pub fn sinh(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose hyperbolic cosine to calculate. /// The angle whose hyperbolic cosine to calculate.
/// ///
/// ## Category /// Display: Hyperbolic cosine
/// calculate /// Category: calculate
#[func] #[func]
pub fn cosh(args: &mut Args) -> SourceResult<Value> { pub fn cosh(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -338,7 +329,6 @@ pub fn cosh(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Hyperbolic tangent
/// Calculate the hyperbolic tangent of an angle. /// Calculate the hyperbolic tangent of an angle.
/// ///
/// When called with an integer or a float, they will be interpreted as radians. /// When called with an integer or a float, they will be interpreted as radians.
@ -353,8 +343,8 @@ pub fn cosh(args: &mut Args) -> SourceResult<Value> {
/// - angle: `AngleLike` (positional, required) /// - angle: `AngleLike` (positional, required)
/// The angle whose hyperbolic tangent to calculate. /// The angle whose hyperbolic tangent to calculate.
/// ///
/// ## Category /// Display: Hyperbolic tangent
/// calculate /// Category: calculate
#[func] #[func]
pub fn tanh(args: &mut Args) -> SourceResult<Value> { pub fn tanh(args: &mut Args) -> SourceResult<Value> {
let arg = args.expect::<AngleLike>("angle")?; let arg = args.expect::<AngleLike>("angle")?;
@ -365,7 +355,6 @@ pub fn tanh(args: &mut Args) -> SourceResult<Value> {
})) }))
} }
/// # Logarithm
/// Calculate the logarithm of a number. /// Calculate the logarithm of a number.
/// ///
/// If the base is not specified, the logarithm is calculated in base 10. /// If the base is not specified, the logarithm is calculated in base 10.
@ -381,8 +370,8 @@ pub fn tanh(args: &mut Args) -> SourceResult<Value> {
/// - base: `Num` (named) /// - base: `Num` (named)
/// The base of the logarithm. /// The base of the logarithm.
/// ///
/// ## Category /// Display: Logarithm
/// calculate /// Category: calculate
#[func] #[func]
pub fn log(args: &mut Args) -> SourceResult<Value> { pub fn log(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<f64>("value")?; let value = args.expect::<f64>("value")?;
@ -390,7 +379,6 @@ pub fn log(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Float(value.log(base))) Ok(Value::Float(value.log(base)))
} }
/// # Round down
/// Round a number down to the nearest integer. /// Round a number down to the nearest integer.
/// ///
/// If the number is already an integer, it is returned unchanged. /// If the number is already an integer, it is returned unchanged.
@ -406,8 +394,8 @@ pub fn log(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number to round down. /// The number to round down.
/// ///
/// ## Category /// Display: Round down
/// calculate /// Category: calculate
#[func] #[func]
pub fn floor(args: &mut Args) -> SourceResult<Value> { pub fn floor(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Num>("value")?; let value = args.expect::<Num>("value")?;
@ -417,7 +405,6 @@ pub fn floor(args: &mut Args) -> SourceResult<Value> {
}) })
} }
/// # Round up
/// Round a number up to the nearest integer. /// Round a number up to the nearest integer.
/// ///
/// If the number is already an integer, it is returned unchanged. /// If the number is already an integer, it is returned unchanged.
@ -433,8 +420,8 @@ pub fn floor(args: &mut Args) -> SourceResult<Value> {
/// - value: `Num` (positional, required) /// - value: `Num` (positional, required)
/// The number to round up. /// The number to round up.
/// ///
/// ## Category /// Display: Round up
/// calculate /// Category: calculate
#[func] #[func]
pub fn ceil(args: &mut Args) -> SourceResult<Value> { pub fn ceil(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Num>("value")?; let value = args.expect::<Num>("value")?;
@ -444,7 +431,6 @@ pub fn ceil(args: &mut Args) -> SourceResult<Value> {
}) })
} }
/// # Round
/// Round a number to the nearest integer. /// Round a number to the nearest integer.
/// ///
/// Optionally, a number of decimal places can be specified. /// Optionally, a number of decimal places can be specified.
@ -461,8 +447,8 @@ pub fn ceil(args: &mut Args) -> SourceResult<Value> {
/// The number to round. /// The number to round.
/// - digits: `i64` (named) /// - digits: `i64` (named)
/// ///
/// ## Category /// Display: Round
/// calculate /// Category: calculate
#[func] #[func]
pub fn round(args: &mut Args) -> SourceResult<Value> { pub fn round(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Num>("value")?; let value = args.expect::<Num>("value")?;
@ -477,7 +463,6 @@ pub fn round(args: &mut Args) -> SourceResult<Value> {
}) })
} }
/// # Clamp
/// Clamp a number between a minimum and maximum value. /// Clamp a number between a minimum and maximum value.
/// ///
/// ## Example /// ## Example
@ -495,8 +480,8 @@ pub fn round(args: &mut Args) -> SourceResult<Value> {
/// - max: `Num` (positional, required) /// - max: `Num` (positional, required)
/// The inclusive maximum value. /// The inclusive maximum value.
/// ///
/// ## Category /// Display: Clamp
/// calculate /// Category: calculate
#[func] #[func]
pub fn clamp(args: &mut Args) -> SourceResult<Value> { pub fn clamp(args: &mut Args) -> SourceResult<Value> {
let value = args.expect::<Num>("value")?; let value = args.expect::<Num>("value")?;
@ -508,7 +493,6 @@ pub fn clamp(args: &mut Args) -> SourceResult<Value> {
Ok(value.apply3(min, max.v, i64::clamp, f64::clamp)) Ok(value.apply3(min, max.v, i64::clamp, f64::clamp))
} }
/// # Minimum
/// Determine the minimum of a sequence of values. /// Determine the minimum of a sequence of values.
/// ///
/// ## Example /// ## Example
@ -524,14 +508,13 @@ pub fn clamp(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: any /// - returns: any
/// ///
/// ## Category /// Display: Minimum
/// calculate /// Category: calculate
#[func] #[func]
pub fn min(args: &mut Args) -> SourceResult<Value> { pub fn min(args: &mut Args) -> SourceResult<Value> {
minmax(args, Ordering::Less) minmax(args, Ordering::Less)
} }
/// # Maximum
/// Determine the maximum of a sequence of values. /// Determine the maximum of a sequence of values.
/// ///
/// ## Example /// ## Example
@ -547,8 +530,8 @@ pub fn min(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: any /// - returns: any
/// ///
/// ## Category /// Display: Maximum
/// calculate /// Category: calculate
#[func] #[func]
pub fn max(args: &mut Args) -> SourceResult<Value> { pub fn max(args: &mut Args) -> SourceResult<Value> {
minmax(args, Ordering::Greater) minmax(args, Ordering::Greater)
@ -575,7 +558,6 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
Ok(extremum) Ok(extremum)
} }
/// # Even
/// Determine whether an integer is even. /// Determine whether an integer is even.
/// ///
/// ## Example /// ## Example
@ -591,14 +573,13 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
/// ///
/// - returns: boolean /// - returns: boolean
/// ///
/// ## Category /// Display: Even
/// calculate /// Category: calculate
#[func] #[func]
pub fn even(args: &mut Args) -> SourceResult<Value> { pub fn even(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Bool(args.expect::<i64>("value")? % 2 == 0)) Ok(Value::Bool(args.expect::<i64>("value")? % 2 == 0))
} }
/// # Odd
/// Determine whether an integer is odd. /// Determine whether an integer is odd.
/// ///
/// ## Example /// ## Example
@ -615,14 +596,13 @@ pub fn even(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: boolean /// - returns: boolean
/// ///
/// ## Category /// Display: Odd
/// calculate /// Category: calculate
#[func] #[func]
pub fn odd(args: &mut Args) -> SourceResult<Value> { pub fn odd(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Bool(args.expect::<i64>("value")? % 2 != 0)) Ok(Value::Bool(args.expect::<i64>("value")? % 2 != 0))
} }
/// # Modulus
/// Calculate the modulus of two numbers. /// Calculate the modulus of two numbers.
/// ///
/// ## Example /// ## Example
@ -640,8 +620,8 @@ pub fn odd(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: integer or float /// - returns: integer or float
/// ///
/// ## Category /// Display: Modulus
/// calculate /// Category: calculate
#[func] #[func]
pub fn mod_(args: &mut Args) -> SourceResult<Value> { pub fn mod_(args: &mut Args) -> SourceResult<Value> {
let dividend = args.expect::<Num>("dividend")?; let dividend = args.expect::<Num>("dividend")?;
@ -693,7 +673,7 @@ impl Num {
} }
} }
castable! { cast_from_value! {
Num, Num,
v: i64 => Self::Int(v), v: i64 => Self::Int(v),
v: f64 => Self::Float(v), v: f64 => Self::Float(v),
@ -706,7 +686,7 @@ enum AngleLike {
Angle(Angle), Angle(Angle),
} }
castable! { cast_from_value! {
AngleLike, AngleLike,
v: i64 => Self::Int(v), v: i64 => Self::Int(v),
v: f64 => Self::Float(v), v: f64 => Self::Float(v),

View File

@ -5,7 +5,6 @@ use typst::eval::Regex;
use crate::prelude::*; use crate::prelude::*;
/// # Integer
/// Convert a value to an integer. /// Convert a value to an integer.
/// ///
/// - Booleans are converted to `0` or `1`. /// - Booleans are converted to `0` or `1`.
@ -26,8 +25,8 @@ use crate::prelude::*;
/// ///
/// - returns: integer /// - returns: integer
/// ///
/// ## Category /// Display: Integer
/// construct /// Category: construct
#[func] #[func]
pub fn int(args: &mut Args) -> SourceResult<Value> { pub fn int(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Int(args.expect::<ToInt>("value")?.0)) Ok(Value::Int(args.expect::<ToInt>("value")?.0))
@ -36,7 +35,7 @@ pub fn int(args: &mut Args) -> SourceResult<Value> {
/// A value that can be cast to an integer. /// A value that can be cast to an integer.
struct ToInt(i64); struct ToInt(i64);
castable! { cast_from_value! {
ToInt, ToInt,
v: bool => Self(v as i64), v: bool => Self(v as i64),
v: i64 => Self(v), v: i64 => Self(v),
@ -44,7 +43,6 @@ castable! {
v: EcoString => Self(v.parse().map_err(|_| "not a valid integer")?), v: EcoString => Self(v.parse().map_err(|_| "not a valid integer")?),
} }
/// # Float
/// Convert a value to a float. /// Convert a value to a float.
/// ///
/// - Booleans are converted to `0.0` or `1.0`. /// - Booleans are converted to `0.0` or `1.0`.
@ -67,8 +65,8 @@ castable! {
/// ///
/// - returns: float /// - returns: float
/// ///
/// ## Category /// Display: Float
/// construct /// Category: construct
#[func] #[func]
pub fn float(args: &mut Args) -> SourceResult<Value> { pub fn float(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Float(args.expect::<ToFloat>("value")?.0)) Ok(Value::Float(args.expect::<ToFloat>("value")?.0))
@ -77,7 +75,7 @@ pub fn float(args: &mut Args) -> SourceResult<Value> {
/// A value that can be cast to a float. /// A value that can be cast to a float.
struct ToFloat(f64); struct ToFloat(f64);
castable! { cast_from_value! {
ToFloat, ToFloat,
v: bool => Self(v as i64 as f64), v: bool => Self(v as i64 as f64),
v: i64 => Self(v as f64), v: i64 => Self(v as f64),
@ -85,7 +83,6 @@ castable! {
v: EcoString => Self(v.parse().map_err(|_| "not a valid float")?), v: EcoString => Self(v.parse().map_err(|_| "not a valid float")?),
} }
/// # Luma
/// Create a grayscale color. /// Create a grayscale color.
/// ///
/// ## Example /// ## Example
@ -101,15 +98,14 @@ castable! {
/// ///
/// - returns: color /// - returns: color
/// ///
/// ## Category /// Display: Luma
/// construct /// Category: construct
#[func] #[func]
pub fn luma(args: &mut Args) -> SourceResult<Value> { pub fn luma(args: &mut Args) -> SourceResult<Value> {
let Component(luma) = args.expect("gray component")?; let Component(luma) = args.expect("gray component")?;
Ok(Value::Color(LumaColor::new(luma).into())) Ok(Value::Color(LumaColor::new(luma).into()))
} }
/// # RGBA
/// Create an RGB(A) color. /// Create an RGB(A) color.
/// ///
/// The color is specified in the sRGB color space. /// The color is specified in the sRGB color space.
@ -154,8 +150,8 @@ pub fn luma(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: color /// - returns: color
/// ///
/// ## Category /// Display: RGBA
/// construct /// Category: construct
#[func] #[func]
pub fn rgb(args: &mut Args) -> SourceResult<Value> { pub fn rgb(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Color(if let Some(string) = args.find::<Spanned<EcoString>>()? { Ok(Value::Color(if let Some(string) = args.find::<Spanned<EcoString>>()? {
@ -175,7 +171,7 @@ pub fn rgb(args: &mut Args) -> SourceResult<Value> {
/// An integer or ratio component. /// An integer or ratio component.
struct Component(u8); struct Component(u8);
castable! { cast_from_value! {
Component, Component,
v: i64 => match v { v: i64 => match v {
0 ..= 255 => Self(v as u8), 0 ..= 255 => Self(v as u8),
@ -188,7 +184,6 @@ castable! {
}, },
} }
/// # CMYK
/// Create a CMYK color. /// Create a CMYK color.
/// ///
/// This is useful if you want to target a specific printer. The conversion /// This is useful if you want to target a specific printer. The conversion
@ -217,8 +212,8 @@ castable! {
/// ///
/// - returns: color /// - returns: color
/// ///
/// ## Category /// Display: CMYK
/// construct /// Category: construct
#[func] #[func]
pub fn cmyk(args: &mut Args) -> SourceResult<Value> { pub fn cmyk(args: &mut Args) -> SourceResult<Value> {
let RatioComponent(c) = args.expect("cyan component")?; let RatioComponent(c) = args.expect("cyan component")?;
@ -231,7 +226,7 @@ pub fn cmyk(args: &mut Args) -> SourceResult<Value> {
/// A component that must be a ratio. /// A component that must be a ratio.
struct RatioComponent(u8); struct RatioComponent(u8);
castable! { cast_from_value! {
RatioComponent, RatioComponent,
v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
Self((v.get() * 255.0).round() as u8) Self((v.get() * 255.0).round() as u8)
@ -240,7 +235,6 @@ castable! {
}, },
} }
/// # Symbol
/// Create a custom symbol with modifiers. /// Create a custom symbol with modifiers.
/// ///
/// ## Example /// ## Example
@ -272,8 +266,8 @@ castable! {
/// ///
/// - returns: symbol /// - returns: symbol
/// ///
/// ## Category /// Display: Symbol
/// construct /// Category: construct
#[func] #[func]
pub fn symbol(args: &mut Args) -> SourceResult<Value> { pub fn symbol(args: &mut Args) -> SourceResult<Value> {
let mut list = EcoVec::new(); let mut list = EcoVec::new();
@ -289,7 +283,7 @@ pub fn symbol(args: &mut Args) -> SourceResult<Value> {
/// A value that can be cast to a symbol. /// A value that can be cast to a symbol.
struct Variant(EcoString, char); struct Variant(EcoString, char);
castable! { cast_from_value! {
Variant, Variant,
c: char => Self(EcoString::new(), c), c: char => Self(EcoString::new(), c),
array: Array => { array: Array => {
@ -301,7 +295,6 @@ castable! {
}, },
} }
/// # String
/// Convert a value to a string. /// Convert a value to a string.
/// ///
/// - Integers are formatted in base 10. /// - Integers are formatted in base 10.
@ -322,8 +315,8 @@ castable! {
/// ///
/// - returns: string /// - returns: string
/// ///
/// ## Category /// Display: String
/// construct /// Category: construct
#[func] #[func]
pub fn str(args: &mut Args) -> SourceResult<Value> { pub fn str(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Str(args.expect::<ToStr>("value")?.0)) Ok(Value::Str(args.expect::<ToStr>("value")?.0))
@ -332,7 +325,7 @@ pub fn str(args: &mut Args) -> SourceResult<Value> {
/// A value that can be cast to a string. /// A value that can be cast to a string.
struct ToStr(Str); struct ToStr(Str);
castable! { cast_from_value! {
ToStr, ToStr,
v: i64 => Self(format_str!("{}", v)), v: i64 => Self(format_str!("{}", v)),
v: f64 => Self(format_str!("{}", v)), v: f64 => Self(format_str!("{}", v)),
@ -340,7 +333,6 @@ castable! {
v: Str => Self(v), v: Str => Self(v),
} }
/// # Label
/// Create a label from a string. /// Create a label from a string.
/// ///
/// Inserting a label into content attaches it to the closest previous element /// Inserting a label into content attaches it to the closest previous element
@ -366,14 +358,13 @@ castable! {
/// ///
/// - returns: label /// - returns: label
/// ///
/// ## Category /// Display: Label
/// construct /// Category: construct
#[func] #[func]
pub fn label(args: &mut Args) -> SourceResult<Value> { pub fn label(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Label(Label(args.expect("string")?))) Ok(Value::Label(Label(args.expect("string")?)))
} }
/// # Regex
/// Create a regular expression from a string. /// Create a regular expression from a string.
/// ///
/// The result can be used as a /// The result can be used as a
@ -406,15 +397,14 @@ pub fn label(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: regex /// - returns: regex
/// ///
/// ## Category /// Display: Regex
/// construct /// Category: construct
#[func] #[func]
pub fn regex(args: &mut Args) -> SourceResult<Value> { pub fn regex(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?; let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
Ok(Regex::new(&v).at(span)?.into()) Ok(Regex::new(&v).at(span)?.into())
} }
/// # Range
/// Create an array consisting of a sequence of numbers. /// Create an array consisting of a sequence of numbers.
/// ///
/// If you pass just one positional parameter, it is interpreted as the `end` of /// If you pass just one positional parameter, it is interpreted as the `end` of
@ -442,8 +432,8 @@ pub fn regex(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: array /// - returns: array
/// ///
/// ## Category /// Display: Range
/// construct /// Category: construct
#[func] #[func]
pub fn range(args: &mut Args) -> SourceResult<Value> { pub fn range(args: &mut Args) -> SourceResult<Value> {
let first = args.expect::<i64>("end")?; let first = args.expect::<i64>("end")?;

View File

@ -4,7 +4,6 @@ use typst::diag::{format_xml_like_error, FileError};
use crate::prelude::*; use crate::prelude::*;
/// # Plain text
/// Read plain text from a file. /// Read plain text from a file.
/// ///
/// The file will be read and returned as a string. /// The file will be read and returned as a string.
@ -23,8 +22,8 @@ use crate::prelude::*;
/// ///
/// - returns: string /// - returns: string
/// ///
/// ## Category /// Display: Plain text
/// data-loading /// Category: data-loading
#[func] #[func]
pub fn read(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn read(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } = args.expect::<Spanned<EcoString>>("path to file")?; let Spanned { v: path, span } = args.expect::<Spanned<EcoString>>("path to file")?;
@ -38,7 +37,6 @@ pub fn read(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
Ok(Value::Str(text.into())) Ok(Value::Str(text.into()))
} }
/// # CSV
/// Read structured data from a CSV file. /// Read structured data from a CSV file.
/// ///
/// The CSV file will be read and parsed into a 2-dimensional array of strings: /// The CSV file will be read and parsed into a 2-dimensional array of strings:
@ -68,8 +66,8 @@ pub fn read(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: array /// - returns: array
/// ///
/// ## Category /// Display: CSV
/// data-loading /// Category: data-loading
#[func] #[func]
pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } = let Spanned { v: path, span } =
@ -100,7 +98,7 @@ pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
/// The delimiter to use when parsing CSV files. /// The delimiter to use when parsing CSV files.
struct Delimiter(u8); struct Delimiter(u8);
castable! { cast_from_value! {
Delimiter, Delimiter,
v: EcoString => { v: EcoString => {
let mut chars = v.chars(); let mut chars = v.chars();
@ -134,7 +132,6 @@ fn format_csv_error(error: csv::Error) -> String {
} }
} }
/// # JSON
/// Read structured data from a JSON file. /// Read structured data from a JSON file.
/// ///
/// The file must contain a valid JSON object or array. JSON objects will be /// The file must contain a valid JSON object or array. JSON objects will be
@ -179,8 +176,8 @@ fn format_csv_error(error: csv::Error) -> String {
/// ///
/// - returns: dictionary or array /// - returns: dictionary or array
/// ///
/// ## Category /// Display: JSON
/// data-loading /// Category: data-loading
#[func] #[func]
pub fn json(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn json(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } = let Spanned { v: path, span } =
@ -222,7 +219,6 @@ fn format_json_error(error: serde_json::Error) -> String {
format!("failed to parse json file: syntax error in line {}", error.line()) format!("failed to parse json file: syntax error in line {}", error.line())
} }
/// # XML
/// Read structured data from an XML file. /// Read structured data from an XML file.
/// ///
/// The XML file is parsed into an array of dictionaries and strings. XML nodes /// The XML file is parsed into an array of dictionaries and strings. XML nodes
@ -278,8 +274,8 @@ fn format_json_error(error: serde_json::Error) -> String {
/// ///
/// - returns: array /// - returns: array
/// ///
/// ## Category /// Display: XML
/// data-loading /// Category: data-loading
#[func] #[func]
pub fn xml(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn xml(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } = let Spanned { v: path, span } =

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Type
/// Determine a value's type. /// Determine a value's type.
/// ///
/// Returns the name of the value's type. /// Returns the name of the value's type.
@ -21,14 +20,13 @@ use crate::prelude::*;
/// ///
/// - returns: string /// - returns: string
/// ///
/// ## Category /// Display: Type
/// foundations /// Category: foundations
#[func] #[func]
pub fn type_(args: &mut Args) -> SourceResult<Value> { pub fn type_(args: &mut Args) -> SourceResult<Value> {
Ok(args.expect::<Value>("value")?.type_name().into()) Ok(args.expect::<Value>("value")?.type_name().into())
} }
/// # Representation
/// The string representation of a value. /// The string representation of a value.
/// ///
/// When inserted into content, most values are displayed as this representation /// When inserted into content, most values are displayed as this representation
@ -49,14 +47,13 @@ pub fn type_(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: string /// - returns: string
/// ///
/// ## Category /// Display: Representation
/// foundations /// Category: foundations
#[func] #[func]
pub fn repr(args: &mut Args) -> SourceResult<Value> { pub fn repr(args: &mut Args) -> SourceResult<Value> {
Ok(args.expect::<Value>("value")?.repr().into()) Ok(args.expect::<Value>("value")?.repr().into())
} }
/// # Panic
/// Fail with an error. /// Fail with an error.
/// ///
/// ## Example /// ## Example
@ -69,8 +66,8 @@ pub fn repr(args: &mut Args) -> SourceResult<Value> {
/// - payload: `Value` (positional) /// - payload: `Value` (positional)
/// The value (or message) to panic with. /// The value (or message) to panic with.
/// ///
/// ## Category /// Display: Panic
/// foundations /// Category: foundations
#[func] #[func]
pub fn panic(args: &mut Args) -> SourceResult<Value> { pub fn panic(args: &mut Args) -> SourceResult<Value> {
match args.eat::<Value>()? { match args.eat::<Value>()? {
@ -79,7 +76,6 @@ pub fn panic(args: &mut Args) -> SourceResult<Value> {
} }
} }
/// # Assert
/// Ensure that a condition is fulfilled. /// Ensure that a condition is fulfilled.
/// ///
/// Fails with an error if the condition is not fulfilled. Does not /// Fails with an error if the condition is not fulfilled. Does not
@ -96,8 +92,8 @@ pub fn panic(args: &mut Args) -> SourceResult<Value> {
/// - message: `EcoString` (named) /// - message: `EcoString` (named)
/// The error message when the assertion fails. /// The error message when the assertion fails.
/// ///
/// ## Category /// Display: Assert
/// foundations /// Category: foundations
#[func] #[func]
pub fn assert(args: &mut Args) -> SourceResult<Value> { pub fn assert(args: &mut Args) -> SourceResult<Value> {
let check = args.expect::<bool>("condition")?; let check = args.expect::<bool>("condition")?;
@ -112,7 +108,6 @@ pub fn assert(args: &mut Args) -> SourceResult<Value> {
Ok(Value::None) Ok(Value::None)
} }
/// # Evaluate
/// Evaluate a string as Typst code. /// Evaluate a string as Typst code.
/// ///
/// This function should only be used as a last resort. /// This function should only be used as a last resort.
@ -132,8 +127,8 @@ pub fn assert(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: any /// - returns: any
/// ///
/// ## Category /// Display: Evaluate
/// foundations /// Category: foundations
#[func] #[func]
pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?; let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?;

View File

@ -1,11 +1,10 @@
//! Computational functions. //! Computational functions.
mod calc; pub mod calc;
mod construct; mod construct;
mod data; mod data;
mod foundations; mod foundations;
pub use self::calc::*;
pub use self::construct::*; pub use self::construct::*;
pub use self::data::*; pub use self::data::*;
pub use self::foundations::*; pub use self::foundations::*;

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Align
/// Align content horizontally and vertically. /// Align content horizontally and vertically.
/// ///
/// ## Example /// ## Example
@ -13,63 +12,59 @@ use crate::prelude::*;
/// A work of art, a visual throne /// A work of art, a visual throne
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Align
/// - body: `Content` (positional, required) /// Category: layout
/// The content to align. #[node(Show)]
/// #[set({
/// - alignment: `Axes<Option<GenAlign>>` (positional, settable)
/// The alignment along both axes.
///
/// Possible values for horizontal alignments are:
/// - `start`
/// - `end`
/// - `left`
/// - `center`
/// - `right`
///
/// The `start` and `end` alignments are relative to the current [text
/// direction]($func/text.dir).
///
/// Possible values for vertical alignments are:
/// - `top`
/// - `horizon`
/// - `bottom`
///
/// To align along both axes at the same time, add the two alignments using
/// the `+` operator to get a `2d alignment`. For example, `top + right`
/// aligns the content to the top right corner.
///
/// ```example
/// #set page(height: 6cm)
/// #set text(lang: "ar")
///
/// مثال
/// #align(
/// end + horizon,
/// rect(inset: 12pt)[ركن]
/// )
/// ```
///
/// ## Category
/// layout
#[func]
#[capable]
#[derive(Debug, Hash)]
pub enum AlignNode {}
#[node]
impl AlignNode {
/// The alignment.
#[property(fold, skip)]
pub const ALIGNS: Axes<Option<GenAlign>> =
Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top));
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
args.expect("body")
}
fn set(...) {
let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default(); let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default();
styles.set(Self::ALIGNS, aligns); styles.set(Self::ALIGNMENT, aligns);
})]
pub struct AlignNode {
/// The content to align.
#[positional]
#[required]
pub body: Content,
/// The alignment along both axes.
///
/// Possible values for horizontal alignments are:
/// - `start`
/// - `end`
/// - `left`
/// - `center`
/// - `right`
///
/// The `start` and `end` alignments are relative to the current [text
/// direction]($func/text.dir).
///
/// Possible values for vertical alignments are:
/// - `top`
/// - `horizon`
/// - `bottom`
///
/// To align along both axes at the same time, add the two alignments using
/// the `+` operator to get a `2d alignment`. For example, `top + right`
/// aligns the content to the top right corner.
///
/// ```example
/// #set page(height: 6cm)
/// #set text(lang: "ar")
///
/// مثال
/// #align(
/// end + horizon,
/// rect(inset: 12pt)[ركن]
/// )
/// ```
#[settable]
#[fold]
#[skip]
#[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))]
pub alignment: Axes<Option<GenAlign>>,
}
impl Show for AlignNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
Ok(self.body())
} }
} }

View File

@ -1,7 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
/// # Columns
/// Separate a region into multiple equally sized columns. /// Separate a region into multiple equally sized columns.
/// ///
/// The `column` function allows to separate the interior of any container into /// The `column` function allows to separate the interior of any container into
@ -31,39 +30,25 @@ use crate::text::TextNode;
/// variety of problems. /// variety of problems.
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Columns
/// - count: `usize` (positional, required) /// Category: layout
/// The number of columns. #[node(Layout)]
///
/// - body: `Content` (positional, required)
/// The content that should be layouted into the columns.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct ColumnsNode { pub struct ColumnsNode {
/// How many columns there should be. /// The number of columns.
#[positional]
#[required]
pub count: NonZeroUsize, pub count: NonZeroUsize,
/// The child to be layouted into the columns. Most likely, this should be a
/// flow or stack node. /// The content that should be layouted into the columns.
#[positional]
#[required]
pub body: Content, pub body: Content,
}
#[node]
impl ColumnsNode {
/// The size of the gutter space between each column. /// The size of the gutter space between each column.
#[property(resolve)] #[settable]
pub const GUTTER: Rel<Length> = Ratio::new(0.04).into(); #[resolve]
#[default(Ratio::new(0.04).into())]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub gutter: Rel<Length>,
Ok(Self {
count: args.expect("column count")?,
body: args.expect("body")?,
}
.pack())
}
} }
impl Layout for ColumnsNode { impl Layout for ColumnsNode {
@ -73,14 +58,16 @@ impl Layout for ColumnsNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let body = self.body();
// Separating the infinite space into infinite columns does not make // Separating the infinite space into infinite columns does not make
// much sense. // much sense.
if !regions.size.x.is_finite() { if !regions.size.x.is_finite() {
return self.body.layout(vt, styles, regions); return body.layout(vt, styles, regions);
} }
// Determine the width of the gutter and each column. // Determine the width of the gutter and each column.
let columns = self.count.get(); let columns = self.count().get();
let gutter = styles.get(Self::GUTTER).relative_to(regions.base().x); let gutter = styles.get(Self::GUTTER).relative_to(regions.base().x);
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64; let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
@ -100,7 +87,7 @@ impl Layout for ColumnsNode {
}; };
// Layout the children. // Layout the children.
let mut frames = self.body.layout(vt, styles, pod)?.into_iter(); let mut frames = body.layout(vt, styles, pod)?.into_iter();
let mut finished = vec![]; let mut finished = vec![];
let dir = styles.get(TextNode::DIR); let dir = styles.get(TextNode::DIR);
@ -140,7 +127,6 @@ impl Layout for ColumnsNode {
} }
} }
/// # Column Break
/// A forced column break. /// A forced column break.
/// ///
/// The function will behave like a [page break]($func/pagebreak) when used in a /// The function will behave like a [page break]($func/pagebreak) when used in a
@ -165,31 +151,20 @@ impl Layout for ColumnsNode {
/// laws of nature. /// laws of nature.
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Column Break
/// - weak: `bool` (named) /// Category: layout
/// If `{true}`, the column break is skipped if the current column is already #[node(Behave)]
/// empty.
///
/// ## Category
/// layout
#[func]
#[capable(Behave)]
#[derive(Debug, Hash)]
pub struct ColbreakNode { pub struct ColbreakNode {
/// If `{true}`, the column break is skipped if the current column is
/// already empty.
#[named]
#[default(false)]
pub weak: bool, pub weak: bool,
} }
#[node]
impl ColbreakNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let weak = args.named("weak")?.unwrap_or(false);
Ok(Self { weak }.pack())
}
}
impl Behave for ColbreakNode { impl Behave for ColbreakNode {
fn behaviour(&self) -> Behaviour { fn behaviour(&self) -> Behaviour {
if self.weak { if self.weak() {
Behaviour::Weak(1) Behaviour::Weak(1)
} else { } else {
Behaviour::Destructive Behaviour::Destructive

View File

@ -2,7 +2,6 @@ use super::VNode;
use crate::layout::Spacing; use crate::layout::Spacing;
use crate::prelude::*; use crate::prelude::*;
/// # Box
/// An inline-level container that sizes content. /// An inline-level container that sizes content.
/// ///
/// All elements except inline math, text, and boxes are block-level and cannot /// All elements except inline math, text, and boxes are block-level and cannot
@ -20,69 +19,75 @@ use crate::prelude::*;
/// for more information. /// for more information.
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Box
/// - body: `Content` (positional) /// Category: layout
/// The contents of the box. #[node(Layout)]
///
/// - width: `Sizing` (named)
/// The width of the box.
///
/// Boxes can have [fractional]($type/fraction) widths, as the example
/// below demonstrates.
///
/// _Note:_ Currently, only boxes and only their widths might be fractionally
/// sized within paragraphs. Support for fractionally sized images, shapes,
/// and more might be added in the future.
///
/// ```example
/// Line in #box(width: 1fr, line(length: 100%)) between.
/// ```
///
/// - height: `Rel<Length>` (named)
/// The height of the box.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct BoxNode { pub struct BoxNode {
/// The box's content. /// The contents of the box.
#[positional]
#[default]
pub body: Content, pub body: Content,
/// The box's width.
pub width: Sizing,
/// The box's height.
pub height: Smart<Rel<Length>>,
}
#[node] /// The width of the box.
impl BoxNode { ///
/// Boxes can have [fractional]($type/fraction) widths, as the example
/// below demonstrates.
///
/// _Note:_ Currently, only boxes and only their widths might be fractionally
/// sized within paragraphs. Support for fractionally sized images, shapes,
/// and more might be added in the future.
///
/// ```example
/// Line in #box(width: 1fr, line(length: 100%)) between.
/// ```
#[named]
#[default]
pub width: Sizing,
/// The height of the box.
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// An amount to shift the box's baseline by. /// An amount to shift the box's baseline by.
/// ///
/// ```example /// ```example
/// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
/// ``` /// ```
#[property(resolve)] #[settable]
pub const BASELINE: Rel<Length> = Rel::zero(); #[resolve]
#[default]
pub baseline: Rel<Length>,
/// The box's background color. See the /// The box's background color. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// The box's border color. See the /// The box's border color. See the
/// [rectangle's documentation]($func/rect.stroke) for more details. /// [rectangle's documentation]($func/rect.stroke) for more details.
#[property(resolve, fold)] #[settable]
pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None); #[resolve]
#[fold]
#[default]
pub stroke: Sides<Option<Option<PartialStroke>>>,
/// How much to round the box's corners. See the [rectangle's /// How much to round the box's corners. See the [rectangle's
/// documentation]($func/rect.radius) for more details. /// documentation]($func/rect.radius) for more details.
#[property(resolve, fold)] #[settable]
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the box's content. See the [rectangle's /// How much to pad the box's content. See the [rectangle's
/// documentation]($func/rect.inset) for more details. /// documentation]($func/rect.inset) for more details.
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the box's size without affecting the layout. /// How much to expand the box's size without affecting the layout.
/// ///
@ -98,15 +103,11 @@ impl BoxNode {
/// outset: (y: 3pt), /// outset: (y: 3pt),
/// radius: 2pt, /// radius: 2pt,
/// )[rectangle]. /// )[rectangle].
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let body = args.eat()?.unwrap_or_default(); pub outset: Sides<Option<Rel<Length>>>,
let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?.unwrap_or_default();
Ok(Self { body, width, height }.pack())
}
} }
impl Layout for BoxNode { impl Layout for BoxNode {
@ -116,14 +117,14 @@ impl Layout for BoxNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let width = match self.width { let width = match self.width() {
Sizing::Auto => Smart::Auto, Sizing::Auto => Smart::Auto,
Sizing::Rel(rel) => Smart::Custom(rel), Sizing::Rel(rel) => Smart::Custom(rel),
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
}; };
// Resolve the sizing to a concrete size. // Resolve the sizing to a concrete size.
let sizing = Axes::new(width, self.height); let sizing = Axes::new(width, self.height());
let expand = sizing.as_ref().map(Smart::is_custom); let expand = sizing.as_ref().map(Smart::is_custom);
let size = sizing let size = sizing
.resolve(styles) .resolve(styles)
@ -132,10 +133,10 @@ impl Layout for BoxNode {
.unwrap_or(regions.base()); .unwrap_or(regions.base());
// Apply inset. // Apply inset.
let mut child = self.body.clone(); let mut child = self.body();
let inset = styles.get(Self::INSET); let inset = styles.get(Self::INSET);
if inset.iter().any(|v| !v.is_zero()) { if inset.iter().any(|v| !v.is_zero()) {
child = child.clone().padded(inset.map(|side| side.map(Length::from))); child = child.padded(inset.map(|side| side.map(Length::from)));
} }
// Select the appropriate base and expansion for the child depending // Select the appropriate base and expansion for the child depending
@ -169,7 +170,6 @@ impl Layout for BoxNode {
} }
} }
/// # Block
/// A block-level container. /// A block-level container.
/// ///
/// Such a container can be used to separate content, size it and give it a /// Such a container can be used to separate content, size it and give it a
@ -201,37 +201,6 @@ impl Layout for BoxNode {
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
/// - body: `Content` (positional)
/// The contents of the block.
///
/// - width: `Smart<Rel<Length>>` (named)
/// The block's width.
///
/// ```example
/// #set align(center)
/// #block(
/// width: 60%,
/// inset: 8pt,
/// fill: silver,
/// lorem(10),
/// )
/// ```
///
/// - height: `Smart<Rel<Length>>` (named)
/// The block's height. When the height is larger than the remaining space on
/// a page and [`breakable`]($func/block.breakable) is `{true}`, the block
/// will continue on the next page with the remaining height.
///
/// ```example
/// #set page(height: 80pt)
/// #set align(center)
/// #block(
/// width: 80%,
/// height: 150%,
/// fill: aqua,
/// )
/// ```
///
/// - spacing: `Spacing` (named, settable) /// - spacing: `Spacing` (named, settable)
/// The spacing around this block. This is shorthand to set `above` and /// The spacing around this block. This is shorthand to set `above` and
/// `below` to the same value. /// `below` to the same value.
@ -245,35 +214,62 @@ impl Layout for BoxNode {
/// A second paragraph. /// A second paragraph.
/// ``` /// ```
/// ///
/// - above: `Spacing` (named, settable) /// Display: Block
/// The spacing between this block and its predecessor. Takes precedence over /// Category: layout
/// `spacing`. Can be used in combination with a show rule to adjust the #[node(Layout)]
/// spacing around arbitrary block-level elements. #[set({
/// let spacing = args.named("spacing")?;
/// The default value is `{1.2em}`. styles.set_opt(
/// Self::ABOVE,
/// - below: `Spacing` (named, settable) args.named("above")?
/// The spacing between this block and its successor. Takes precedence .map(VNode::block_around)
/// over `spacing`. .or_else(|| spacing.map(VNode::block_spacing)),
/// );
/// The default value is `{1.2em}`. styles.set_opt(
/// Self::BELOW,
/// ## Category args.named("below")?
/// layout .map(VNode::block_around)
#[func] .or_else(|| spacing.map(VNode::block_spacing)),
#[capable(Layout)] );
#[derive(Debug, Hash)] })]
pub struct BlockNode { pub struct BlockNode {
/// The block's content. /// The contents of the block.
#[positional]
#[default]
pub body: Content, pub body: Content,
/// The box's width.
pub width: Smart<Rel<Length>>,
/// The box's height.
pub height: Smart<Rel<Length>>,
}
#[node] /// The block's width.
impl BlockNode { ///
/// ```example
/// #set align(center)
/// #block(
/// width: 60%,
/// inset: 8pt,
/// fill: silver,
/// lorem(10),
/// )
/// ```
#[named]
#[default]
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 block
/// will continue on the next page with the remaining height.
///
/// ```example
/// #set page(height: 80pt)
/// #set align(center)
/// #block(
/// width: 80%,
/// height: 150%,
/// fill: aqua,
/// )
/// ```
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// Whether the block can be broken and continue on the next page. /// Whether the block can be broken and continue on the next page.
/// ///
/// Defaults to `{true}`. /// Defaults to `{true}`.
@ -286,64 +282,74 @@ impl BlockNode {
/// lorem(15), /// lorem(15),
/// ) /// )
/// ``` /// ```
pub const BREAKABLE: bool = true; #[settable]
#[default(true)]
pub breakable: bool,
/// The block's background color. See the /// The block's background color. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// The block's border color. See the /// The block's border color. See the
/// [rectangle's documentation]($func/rect.stroke) for more details. /// [rectangle's documentation]($func/rect.stroke) for more details.
#[property(resolve, fold)] #[settable]
pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None); #[resolve]
#[fold]
#[default]
pub stroke: Sides<Option<Option<PartialStroke>>>,
/// How much to round the block's corners. See the [rectangle's /// How much to round the block's corners. See the [rectangle's
/// documentation]($func/rect.radius) for more details. /// documentation]($func/rect.radius) for more details.
#[property(resolve, fold)] #[settable]
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the block's content. See the [rectangle's /// How much to pad the block's content. See the [rectangle's
/// documentation]($func/rect.inset) for more details. /// documentation]($func/rect.inset) for more details.
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the block's size without affecting the layout. See /// How much to expand the block's size without affecting the layout. See
/// the [rectangle's documentation]($func/rect.outset) for more details. /// the [rectangle's documentation]($func/rect.outset) for more details.
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub outset: Sides<Option<Rel<Length>>>,
/// The spacing between the previous and this block. /// The spacing between this block and its predecessor. Takes precedence over
#[property(skip)] /// `spacing`. Can be used in combination with a show rule to adjust the
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into()); /// spacing around arbitrary block-level elements.
///
/// The default value is `{1.2em}`.
#[settable]
#[skip]
#[default(VNode::block_spacing(Em::new(1.2).into()))]
pub above: VNode,
/// The spacing between this and the following block. /// The spacing between this block and its successor. Takes precedence
#[property(skip)] /// over `spacing`.
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into()); ///
/// The default value is `{1.2em}`.
#[settable]
#[skip]
#[default(VNode::block_spacing(Em::new(1.2).into()))]
pub below: VNode,
/// Whether this block must stick to the following one. /// Whether this block must stick to the following one.
/// ///
/// Use this to prevent page breaks between e.g. a heading and its body. /// Use this to prevent page breaks between e.g. a heading and its body.
#[property(skip)] #[settable]
pub const STICKY: bool = false; #[skip]
#[default(false)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub sticky: bool,
let body = args.eat()?.unwrap_or_default();
let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?.unwrap_or_default();
Ok(Self { body, width, height }.pack())
}
fn set(...) {
let spacing = args.named("spacing")?.map(VNode::block_spacing);
styles.set_opt(
Self::ABOVE,
args.named("above")?.map(VNode::block_around).or(spacing),
);
styles.set_opt(
Self::BELOW,
args.named("below")?.map(VNode::block_around).or(spacing),
);
}
} }
impl Layout for BlockNode { impl Layout for BlockNode {
@ -354,14 +360,14 @@ impl Layout for BlockNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Apply inset. // Apply inset.
let mut child = self.body.clone(); let mut child = self.body();
let inset = styles.get(Self::INSET); let inset = styles.get(Self::INSET);
if inset.iter().any(|v| !v.is_zero()) { if inset.iter().any(|v| !v.is_zero()) {
child = child.clone().padded(inset.map(|side| side.map(Length::from))); child = child.clone().padded(inset.map(|side| side.map(Length::from)));
} }
// Resolve the sizing to a concrete size. // Resolve the sizing to a concrete size.
let sizing = Axes::new(self.width, self.height); let sizing = Axes::new(self.width(), self.height());
let mut expand = sizing.as_ref().map(Smart::is_custom); let mut expand = sizing.as_ref().map(Smart::is_custom);
let mut size = sizing let mut size = sizing
.resolve(styles) .resolve(styles)
@ -372,7 +378,7 @@ impl Layout for BlockNode {
// Layout the child. // Layout the child.
let mut frames = if styles.get(Self::BREAKABLE) { let mut frames = if styles.get(Self::BREAKABLE) {
// Measure to ensure frames for all regions have the same width. // Measure to ensure frames for all regions have the same width.
if self.width == Smart::Auto { if sizing.x == Smart::Auto {
let pod = Regions::one(size, Axes::splat(false)); let pod = Regions::one(size, Axes::splat(false));
let frame = child.layout(vt, styles, pod)?.into_frame(); let frame = child.layout(vt, styles, pod)?.into_frame();
size.x = frame.width(); size.x = frame.width();
@ -385,7 +391,7 @@ impl Layout for BlockNode {
// Generate backlog for fixed height. // Generate backlog for fixed height.
let mut heights = vec![]; let mut heights = vec![];
if self.height.is_custom() { if sizing.y.is_custom() {
let mut remaining = size.y; let mut remaining = size.y;
for region in regions.iter() { for region in regions.iter() {
let limited = region.y.min(remaining); let limited = region.y.min(remaining);
@ -454,18 +460,6 @@ impl Sizing {
pub fn is_fractional(self) -> bool { pub fn is_fractional(self) -> bool {
matches!(self, Self::Fr(_)) matches!(self, Self::Fr(_))
} }
pub fn encode(self) -> Value {
match self {
Self::Auto => Value::Auto,
Self::Rel(rel) => Spacing::Rel(rel).encode(),
Self::Fr(fr) => Spacing::Fr(fr).encode(),
}
}
pub fn encode_slice(vec: &[Sizing]) -> Value {
Value::Array(vec.iter().copied().map(Self::encode).collect())
}
} }
impl Default for Sizing { impl Default for Sizing {
@ -474,11 +468,26 @@ impl Default for Sizing {
} }
} }
impl From<Spacing> for Sizing { impl<T: Into<Spacing>> From<T> for Sizing {
fn from(spacing: Spacing) -> Self { fn from(spacing: T) -> Self {
match spacing { match spacing.into() {
Spacing::Rel(rel) => Self::Rel(rel), Spacing::Rel(rel) => Self::Rel(rel),
Spacing::Fr(fr) => Self::Fr(fr), Spacing::Fr(fr) => Self::Fr(fr),
} }
} }
} }
cast_from_value! {
Sizing,
_: Smart<Never> => Self::Auto,
v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(v),
}
cast_to_value! {
v: Sizing => match v {
Sizing::Auto => Value::Auto,
Sizing::Rel(rel) => Value::Relative(rel),
Sizing::Fr(fr) => Value::Fraction(fr),
}
}

View File

@ -1,11 +1,12 @@
use std::str::FromStr; use std::str::FromStr;
use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; use crate::layout::{BlockNode, ParNode, Sizing, Spacing};
use crate::meta::{Numbering, NumberingPattern}; use crate::meta::{Numbering, NumberingPattern};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
/// # Numbered List use super::GridLayouter;
/// A numbered list. /// A numbered list.
/// ///
/// Displays a sequence of items vertically and numbers them consecutively. /// Displays a sequence of items vertically and numbers them consecutively.
@ -89,20 +90,19 @@ use crate::text::TextNode;
/// items. /// items.
/// ``` /// ```
/// ///
/// ## Category /// Display: Numbered List
/// layout /// Category: layout
#[func] #[node(Construct, Layout)]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct EnumNode { pub struct EnumNode {
/// If true, the items are separated by leading instead of list spacing. /// The numbered list's items.
pub tight: bool, #[variadic]
/// The individual numbered items. pub items: Vec<EnumItem>,
pub items: StyleVec<(Option<NonZeroUsize>, Content)>,
} /// If true, the items are separated by leading instead of list spacing.
#[named]
#[default(true)]
pub tight: bool,
#[node]
impl EnumNode {
/// How to number the enumeration. Accepts a /// How to number the enumeration. Accepts a
/// [numbering pattern or function]($func/numbering). /// [numbering pattern or function]($func/numbering).
/// ///
@ -122,9 +122,9 @@ impl EnumNode {
/// + Superscript /// + Superscript
/// + Numbering! /// + Numbering!
/// ``` /// ```
#[property(referenced)] #[settable]
pub const NUMBERING: Numbering = #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()); pub numbering: Numbering,
/// Whether to display the full numbering, including the numbers of /// Whether to display the full numbering, including the numbers of
/// all parent enumerations. /// all parent enumerations.
@ -138,64 +138,52 @@ impl EnumNode {
/// + Add integredients /// + Add integredients
/// + Eat /// + Eat
/// ``` /// ```
pub const FULL: bool = false; #[settable]
#[default(false)]
pub full: bool,
/// The indentation of each item's label. /// The indentation of each item's label.
#[property(resolve)] #[settable]
pub const INDENT: Length = Length::zero(); #[resolve]
#[default]
pub indent: Length,
/// The space between the numbering and the body of each item. /// The space between the numbering and the body of each item.
#[property(resolve)] #[settable]
pub const BODY_INDENT: Length = Em::new(0.5).into(); #[resolve]
#[default(Em::new(0.5).into())]
pub body_indent: Length,
/// The spacing between the items of a wide (non-tight) enumeration. /// 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]($func/block.below).
pub const SPACING: Smart<Spacing> = Smart::Auto; #[settable]
#[default]
pub spacing: Smart<Spacing>,
/// The numbers of parent items. /// The numbers of parent items.
#[property(skip, fold)] #[settable]
const PARENTS: Parent = vec![]; #[fold]
#[skip]
#[default]
parents: Parent,
}
impl Construct for EnumNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let mut number: NonZeroUsize = let mut items = args.all::<EnumItem>()?;
args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap()); if let Some(number) = args.named::<NonZeroUsize>("start")? {
if let Some(first) = items.first_mut() {
Ok(Self { if first.number().is_none() {
tight: args.named("tight")?.unwrap_or(true), *first = EnumItem::new(first.body()).with_number(Some(number));
items: args
.all()?
.into_iter()
.map(|body| {
let item = (Some(number), body);
number = number.saturating_add(1);
item
})
.collect(),
} }
}
}
Ok(Self::new(items)
.with_tight(args.named("tight")?.unwrap_or(true))
.pack()) .pack())
} }
fn field(&self, name: &str) -> Option<Value> {
match name {
"tight" => Some(Value::Bool(self.tight)),
"items" => Some(Value::Array(
self.items
.items()
.map(|(number, body)| {
Value::Dict(dict! {
"number" => match *number {
Some(n) => Value::Int(n.get() as i64),
None => Value::None,
},
"body" => Value::Content(body.clone()),
})
})
.collect(),
)),
_ => None,
}
}
} }
impl Layout for EnumNode { impl Layout for EnumNode {
@ -208,12 +196,12 @@ impl Layout for EnumNode {
let numbering = styles.get(Self::NUMBERING); let numbering = styles.get(Self::NUMBERING);
let indent = styles.get(Self::INDENT); let indent = styles.get(Self::INDENT);
let body_indent = styles.get(Self::BODY_INDENT); let body_indent = styles.get(Self::BODY_INDENT);
let gutter = if self.tight { let gutter = if self.tight() {
styles.get(ParNode::LEADING).into() styles.get(ParNode::LEADING).into()
} else { } else {
styles styles
.get(Self::SPACING) .get(Self::SPACING)
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount())
}; };
let mut cells = vec![]; let mut cells = vec![];
@ -221,8 +209,8 @@ impl Layout for EnumNode {
let mut parents = styles.get(Self::PARENTS); let mut parents = styles.get(Self::PARENTS);
let full = styles.get(Self::FULL); let full = styles.get(Self::FULL);
for ((n, item), map) in self.items.iter() { for item in self.items() {
number = n.unwrap_or(number); number = item.number().unwrap_or(number);
let resolved = if full { let resolved = if full {
parents.push(number); parents.push(number);
@ -230,7 +218,7 @@ impl Layout for EnumNode {
parents.pop(); parents.pop();
content content
} else { } else {
match numbering { match &numbering {
Numbering::Pattern(pattern) => { Numbering::Pattern(pattern) => {
TextNode::packed(pattern.apply_kth(parents.len(), number)) TextNode::packed(pattern.apply_kth(parents.len(), number))
} }
@ -239,33 +227,68 @@ impl Layout for EnumNode {
}; };
cells.push(Content::empty()); cells.push(Content::empty());
cells.push(resolved.styled_with_map(map.clone())); cells.push(resolved);
cells.push(Content::empty()); cells.push(Content::empty());
cells.push( cells.push(item.body().styled(Self::PARENTS, Parent(number)));
item.clone()
.styled_with_map(map.clone())
.styled(Self::PARENTS, Parent(number)),
);
number = number.saturating_add(1); number = number.saturating_add(1);
} }
GridNode { let layouter = GridLayouter::new(
tracks: Axes::with_x(vec![ vt,
Axes::with_x(&[
Sizing::Rel(indent.into()), Sizing::Rel(indent.into()),
Sizing::Auto, Sizing::Auto,
Sizing::Rel(body_indent.into()), Sizing::Rel(body_indent.into()),
Sizing::Auto, Sizing::Auto,
]), ]),
gutter: Axes::with_y(vec![gutter.into()]), Axes::with_y(&[gutter.into()]),
cells, &cells,
} regions,
.layout(vt, styles, regions) styles,
);
Ok(layouter.layout()?.fragment)
} }
} }
#[derive(Debug, Clone, Hash)] /// An enumeration item.
#[node]
pub struct EnumItem {
/// The item's number.
#[positional]
#[default]
pub number: Option<NonZeroUsize>,
/// The item's body.
#[positional]
#[required]
pub body: Content,
}
cast_from_value! {
EnumItem,
array: Array => {
let mut iter = array.into_iter();
let (number, body) = match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => (a.cast()?, b.cast()?),
_ => Err("array must contain exactly two entries")?,
};
Self::new(body).with_number(number)
},
v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
}
struct Parent(NonZeroUsize); struct Parent(NonZeroUsize);
cast_from_value! {
Parent,
v: NonZeroUsize => Self(v),
}
cast_to_value! {
v: Parent => v.0.into()
}
impl Fold for Parent { impl Fold for Parent {
type Output = Vec<NonZeroUsize>; type Output = Vec<NonZeroUsize>;

View File

@ -1,4 +1,4 @@
use typst::model::Style; use typst::model::{Style, StyledNode};
use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode}; use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode};
use crate::prelude::*; use crate::prelude::*;
@ -8,12 +8,12 @@ use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode}
/// ///
/// This node is responsible for layouting both the top-level content flow and /// This node is responsible for layouting both the top-level content flow and
/// the contents of boxes. /// the contents of boxes.
#[capable(Layout)] #[node(Layout)]
#[derive(Hash)] pub struct FlowNode {
pub struct FlowNode(pub StyleVec<Content>); /// The children that will be arranges into a flow.
#[variadic]
#[node] pub children: Vec<Content>,
impl FlowNode {} }
impl Layout for FlowNode { impl Layout for FlowNode {
fn layout( fn layout(
@ -24,9 +24,17 @@ impl Layout for FlowNode {
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut layouter = FlowLayouter::new(regions); let mut layouter = FlowLayouter::new(regions);
for (child, map) in self.0.iter() { for mut child in self.children() {
let styles = styles.chain(&map); let map;
if let Some(&node) = child.to::<VNode>() { let outer = styles;
let mut styles = outer;
if let Some(node) = child.to::<StyledNode>() {
map = node.map();
styles = outer.chain(&map);
child = node.sub();
}
if let Some(node) = child.to::<VNode>() {
layouter.layout_spacing(node, styles); layouter.layout_spacing(node, styles);
} else if let Some(node) = child.to::<ParNode>() { } else if let Some(node) = child.to::<ParNode>() {
let barrier = Style::Barrier(child.id()); let barrier = Style::Barrier(child.id());
@ -40,16 +48,16 @@ impl Layout for FlowNode {
{ {
let barrier = Style::Barrier(child.id()); let barrier = Style::Barrier(child.id());
let styles = styles.chain_one(&barrier); let styles = styles.chain_one(&barrier);
layouter.layout_single(vt, child, styles)?; layouter.layout_single(vt, &child, styles)?;
} else if child.has::<dyn Layout>() { } else if child.has::<dyn Layout>() {
layouter.layout_multiple(vt, child, styles)?; layouter.layout_multiple(vt, &child, styles)?;
} else if child.is::<ColbreakNode>() { } else if child.is::<ColbreakNode>() {
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
{ {
layouter.finish_region(); layouter.finish_region();
} }
} else { } else if let Some(span) = child.span() {
panic!("unexpected flow child: {child:?}"); bail!(span, "unexpected flow child");
} }
} }
@ -57,13 +65,6 @@ impl Layout for FlowNode {
} }
} }
impl Debug for FlowNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Flow ")?;
self.0.fmt(f)
}
}
/// Performs flow layout. /// Performs flow layout.
struct FlowLayouter<'a> { struct FlowLayouter<'a> {
/// The regions to layout children into. /// The regions to layout children into.
@ -113,11 +114,11 @@ impl<'a> FlowLayouter<'a> {
} }
/// Layout vertical spacing. /// Layout vertical spacing.
fn layout_spacing(&mut self, node: VNode, styles: StyleChain) { fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) {
self.layout_item(match node.amount { self.layout_item(match node.amount() {
Spacing::Rel(v) => FlowItem::Absolute( Spacing::Rel(v) => FlowItem::Absolute(
v.resolve(styles).relative_to(self.initial.y), v.resolve(styles).relative_to(self.initial.y),
node.weakness > 0, node.weakness() > 0,
), ),
Spacing::Fr(v) => FlowItem::Fractional(v), Spacing::Fr(v) => FlowItem::Fractional(v),
}); });
@ -130,7 +131,7 @@ impl<'a> FlowLayouter<'a> {
par: &ParNode, par: &ParNode,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles);
let leading = styles.get(ParNode::LEADING); let leading = styles.get(ParNode::LEADING);
let consecutive = self.last_was_par; let consecutive = self.last_was_par;
let frames = par let frames = par
@ -176,7 +177,7 @@ impl<'a> FlowLayouter<'a> {
content: &Content, content: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles);
let sticky = styles.get(BlockNode::STICKY); let sticky = styles.get(BlockNode::STICKY);
let pod = Regions::one(self.regions.base(), Axes::splat(false)); let pod = Regions::one(self.regions.base(), Axes::splat(false));
let layoutable = content.with::<dyn Layout>().unwrap(); let layoutable = content.with::<dyn Layout>().unwrap();
@ -204,7 +205,7 @@ impl<'a> FlowLayouter<'a> {
} }
// How to align the block. // How to align the block.
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles);
// Layout the block itself. // Layout the block itself.
let sticky = styles.get(BlockNode::STICKY); let sticky = styles.get(BlockNode::STICKY);

View File

@ -3,7 +3,6 @@ use crate::text::TextNode;
use super::Sizing; use super::Sizing;
/// # Grid
/// Arrange content in a grid. /// Arrange content in a grid.
/// ///
/// The grid element allows you to arrange content in a grid. You can define the /// The grid element allows you to arrange content in a grid. You can define the
@ -61,64 +60,50 @@ use super::Sizing;
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
/// - cells: `Content` (positional, variadic) The contents of the table cells. /// - gutter: `TrackSizings` (named)
/// /// Defines the gaps between rows & columns.
/// The cells are populated in row-major order.
///
/// - rows: `TrackSizings` (named) Defines the row sizes.
///
/// If there are more cells than fit the defined rows, the last row is
/// repeated until there are no more cells.
///
/// - columns: `TrackSizings` (named) Defines the column sizes.
///
/// Either specify a track size array or provide an integer to create a grid
/// with that many `{auto}`-sized columns. Note that opposed to rows and
/// gutters, providing a single track size will only ever create a single
/// column.
///
/// - gutter: `TrackSizings` (named) Defines the gaps between rows & columns.
/// ///
/// If there are more gutters than defined sizes, the last gutter is repeated. /// If there are more gutters than defined sizes, the last gutter is repeated.
/// ///
/// - column-gutter: `TrackSizings` (named) Defines the gaps between columns. /// Display: Grid
/// Takes precedence over `gutter`. /// Category: layout
/// #[node(Layout)]
/// - row-gutter: `TrackSizings` (named) Defines the gaps between rows. Takes
/// precedence over `gutter`.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct GridNode { pub struct GridNode {
/// Defines sizing for content rows and columns. /// The contents of the table cells.
pub tracks: Axes<Vec<Sizing>>, ///
/// Defines sizing of gutter rows and columns between content. /// The cells are populated in row-major order.
pub gutter: Axes<Vec<Sizing>>, #[variadic]
/// The content to be arranged in a grid.
pub cells: Vec<Content>, pub cells: Vec<Content>,
}
#[node] /// Defines the column sizes.
impl GridNode { ///
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { /// Either specify a track size array or provide an integer to create a grid
let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); /// with that many `{auto}`-sized columns. Note that opposed to rows and
let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); /// gutters, providing a single track size will only ever create a single
let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); /// column.
let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); #[named]
let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); #[default]
Ok(Self { pub columns: TrackSizings,
tracks: Axes::new(columns, rows),
gutter: Axes::new( /// Defines the row sizes.
column_gutter.unwrap_or_else(|| base_gutter.clone()), ///
row_gutter.unwrap_or(base_gutter), /// If there are more cells than fit the defined rows, the last row is
), /// repeated until there are no more cells.
cells: args.all()?, #[named]
} #[default]
.pack()) pub rows: TrackSizings,
}
/// Defines the gaps between columns. Takes precedence over `gutter`.
#[named]
#[shorthand(gutter)]
#[default]
pub column_gutter: TrackSizings,
/// Defines the gaps between rows. Takes precedence over `gutter`.
#[named]
#[shorthand(gutter)]
#[default]
pub row_gutter: TrackSizings,
} }
impl Layout for GridNode { impl Layout for GridNode {
@ -129,11 +114,12 @@ impl Layout for GridNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare grid layout by unifying content and gutter tracks. // Prepare grid layout by unifying content and gutter tracks.
let cells = self.cells();
let layouter = GridLayouter::new( let layouter = GridLayouter::new(
vt, vt,
self.tracks.as_deref(), Axes::new(&self.columns().0, &self.rows().0),
self.gutter.as_deref(), Axes::new(&self.column_gutter().0, &self.row_gutter().0),
&self.cells, &cells,
regions, regions,
styles, styles,
); );
@ -147,18 +133,15 @@ impl Layout for GridNode {
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TrackSizings(pub Vec<Sizing>); pub struct TrackSizings(pub Vec<Sizing>);
castable! { cast_from_value! {
TrackSizings, TrackSizings,
sizing: Sizing => Self(vec![sizing]), sizing: Sizing => Self(vec![sizing]),
count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]), count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]),
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
} }
castable! { cast_to_value! {
Sizing, v: TrackSizings => v.0.into()
_: AutoValue => Self::Auto,
v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(v),
} }
/// Performs grid layout. /// Performs grid layout.

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Hide
/// Hide content without affecting layout. /// Hide content without affecting layout.
/// ///
/// The `hide` function allows you to hide content while the layout still 'sees' /// The `hide` function allows you to hide content while the layout still 'sees'
@ -14,26 +13,18 @@ use crate::prelude::*;
/// #hide[Hello] Joe /// #hide[Hello] Joe
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Hide
/// - body: `Content` (positional, required) /// Category: layout
/// The content to hide. #[node(Show)]
/// pub struct HideNode {
/// ## Category /// The content to hide.
/// layout #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct HideNode(pub Content);
#[node]
impl HideNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl Show for HideNode { impl Show for HideNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(Meta::DATA, vec![Meta::Hidden])) Ok(self.body().styled(MetaNode::DATA, vec![Meta::Hidden]))
} }
} }

View File

@ -1,8 +1,9 @@
use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; use crate::layout::{BlockNode, ParNode, Sizing, Spacing};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
/// # Bullet List use super::GridLayouter;
/// A bullet list. /// A bullet list.
/// ///
/// Displays a sequence of items vertically, with each item introduced by a /// Displays a sequence of items vertically, with each item introduced by a
@ -33,48 +34,40 @@ use crate::text::TextNode;
/// paragraphs and other block-level content. All content that is indented /// paragraphs and other block-level content. All content that is indented
/// more than an item's hyphen becomes part of that item. /// more than an item's hyphen becomes part of that item.
/// ///
/// ## Parameters /// Display: Bullet List
/// - items: `Content` (positional, variadic) /// Category: layout
/// The list's children. #[node(Layout)]
///
/// When using the list syntax, adjacent items are automatically collected
/// into lists, even through constructs like for loops.
///
/// ```example
/// #for letter in "ABC" [
/// - Letter #letter
/// ]
/// ```
///
/// - tight: `bool` (named)
/// 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.
///
/// ```example
/// - If a list has a lot of text, and
/// maybe other inline content, it
/// should not be tight anymore.
///
/// - To make a list wide, simply insert
/// a blank line between the items.
/// ```
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct ListNode { pub struct ListNode {
/// If true, the items are separated by leading instead of list spacing. /// The bullet list's children.
pub tight: bool, ///
/// The individual bulleted or numbered items. /// When using the list syntax, adjacent items are automatically collected
pub items: StyleVec<Content>, /// into lists, even through constructs like for loops.
} ///
/// ```example
/// #for letter in "ABC" [
/// - Letter #letter
/// ]
/// ```
#[variadic]
pub items: Vec<ListItem>,
/// 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.
///
/// ```example
/// - If a list has a lot of text, and
/// maybe other inline content, it
/// should not be tight anymore.
///
/// - To make a list wide, simply insert
/// a blank line between the items.
/// ```
#[named]
#[default(true)]
pub tight: bool,
#[node]
impl ListNode {
/// The marker which introduces each item. /// The marker which introduces each item.
/// ///
/// Instead of plain content, you can also pass an array with multiple /// Instead of plain content, you can also pass an array with multiple
@ -96,43 +89,35 @@ impl ListNode {
/// - Items /// - Items
/// - Items /// - Items
/// ``` /// ```
#[property(referenced)] #[settable]
pub const MARKER: Marker = Marker::Content(vec![]); #[default(ListMarker::Content(vec![]))]
pub marker: ListMarker,
/// The indent of each item's marker. /// The indent of each item's marker.
#[property(resolve)] #[settable]
pub const INDENT: Length = Length::zero(); #[resolve]
#[default]
pub indent: Length,
/// The spacing between the marker and the body of each item. /// The spacing between the marker and the body of each item.
#[property(resolve)] #[settable]
pub const BODY_INDENT: Length = Em::new(0.5).into(); #[resolve]
#[default(Em::new(0.5).into())]
pub body_indent: Length,
/// The spacing between the items of a wide (non-tight) list. /// 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]($func/block.below).
pub const SPACING: Smart<Spacing> = Smart::Auto; #[settable]
#[default]
pub spacing: Smart<Spacing>,
/// The nesting depth. /// The nesting depth.
#[property(skip, fold)] #[settable]
const DEPTH: Depth = 0; #[fold]
#[skip]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
Ok(Self { depth: Depth,
tight: args.named("tight")?.unwrap_or(true),
items: args.all()?.into_iter().collect(),
}
.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"tight" => Some(Value::Bool(self.tight)),
"items" => Some(Value::Array(
self.items.items().cloned().map(Value::Content).collect(),
)),
_ => None,
}
}
} }
impl Layout for ListNode { impl Layout for ListNode {
@ -144,49 +129,65 @@ impl Layout for ListNode {
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = styles.get(Self::INDENT); let indent = styles.get(Self::INDENT);
let body_indent = styles.get(Self::BODY_INDENT); let body_indent = styles.get(Self::BODY_INDENT);
let gutter = if self.tight { let gutter = if self.tight() {
styles.get(ParNode::LEADING).into() styles.get(ParNode::LEADING).into()
} else { } else {
styles styles
.get(Self::SPACING) .get(Self::SPACING)
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount())
}; };
let depth = styles.get(Self::DEPTH); let depth = styles.get(Self::DEPTH);
let marker = styles.get(Self::MARKER).resolve(vt.world(), depth)?; let marker = styles.get(Self::MARKER).resolve(vt.world(), depth)?;
let mut cells = vec![]; let mut cells = vec![];
for (item, map) in self.items.iter() { for item in self.items() {
cells.push(Content::empty()); cells.push(Content::empty());
cells.push(marker.clone()); cells.push(marker.clone());
cells.push(Content::empty()); cells.push(Content::empty());
cells.push( cells.push(item.body().styled(Self::DEPTH, Depth));
item.clone().styled_with_map(map.clone()).styled(Self::DEPTH, Depth),
);
} }
GridNode { let layouter = GridLayouter::new(
tracks: Axes::with_x(vec![ vt,
Axes::with_x(&[
Sizing::Rel(indent.into()), Sizing::Rel(indent.into()),
Sizing::Auto, Sizing::Auto,
Sizing::Rel(body_indent.into()), Sizing::Rel(body_indent.into()),
Sizing::Auto, Sizing::Auto,
]), ]),
gutter: Axes::with_y(vec![gutter.into()]), Axes::with_y(&[gutter.into()]),
cells, &cells,
} regions,
.layout(vt, styles, regions) styles,
);
Ok(layouter.layout()?.fragment)
} }
} }
/// A bullet list item.
#[node]
pub struct ListItem {
/// The item's body.
#[positional]
#[required]
pub body: Content,
}
cast_from_value! {
ListItem,
v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
}
/// A list's marker. /// A list's marker.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum Marker { pub enum ListMarker {
Content(Vec<Content>), Content(Vec<Content>),
Func(Func), Func(Func),
} }
impl Marker { impl ListMarker {
/// Resolve the marker for the given depth. /// Resolve the marker for the given depth.
fn resolve(&self, world: Tracked<dyn World>, depth: usize) -> SourceResult<Content> { fn resolve(&self, world: Tracked<dyn World>, depth: usize) -> SourceResult<Content> {
Ok(match self { Ok(match self {
@ -203,8 +204,8 @@ impl Marker {
} }
} }
castable! { cast_from_value! {
Marker, ListMarker,
v: Content => Self::Content(vec![v]), v: Content => Self::Content(vec![v]),
array: Array => { array: Array => {
if array.len() == 0 { if array.len() == 0 {
@ -215,14 +216,28 @@ castable! {
v: Func => Self::Func(v), v: Func => Self::Func(v),
} }
#[derive(Debug, Clone, Hash)] cast_to_value! {
v: ListMarker => match v {
ListMarker::Content(vec) => vec.into(),
ListMarker::Func(func) => func.into(),
}
}
struct Depth; struct Depth;
cast_from_value! {
Depth,
_: Value => Self,
}
cast_to_value! {
_: Depth => Value::None
}
impl Fold for Depth { impl Fold for Depth {
type Output = usize; type Output = usize;
fn fold(self, mut outer: Self::Output) -> Self::Output { fn fold(self, outer: Self::Output) -> Self::Output {
outer += 1; outer + 1
outer
} }
} }

View File

@ -48,8 +48,8 @@ use std::mem;
use typed_arena::Arena; use typed_arena::Arena;
use typst::diag::SourceResult; use typst::diag::SourceResult;
use typst::model::{ use typst::model::{
applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain, applicable, realize, Content, Node, SequenceNode, Style, StyleChain, StyleVecBuilder,
StyleVecBuilder, StyledNode, StyledNode,
}; };
use crate::math::{FormulaNode, LayoutMath}; use crate::math::{FormulaNode, LayoutMath};
@ -60,7 +60,6 @@ use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode};
use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode}; use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode};
/// Root-level layout. /// Root-level layout.
#[capability]
pub trait LayoutRoot { pub trait LayoutRoot {
/// Layout into one frame per page. /// Layout into one frame per page.
fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>;
@ -96,7 +95,6 @@ impl LayoutRoot for Content {
} }
/// Layout into regions. /// Layout into regions.
#[capability]
pub trait Layout { pub trait Layout {
/// Layout into one frame per region. /// Layout into one frame per region.
fn layout( fn layout(
@ -160,7 +158,7 @@ fn realize_root<'a>(
builder.accept(content, styles)?; builder.accept(content, styles)?;
builder.interrupt_page(Some(styles))?; builder.interrupt_page(Some(styles))?;
let (pages, shared) = builder.doc.unwrap().pages.finish(); let (pages, shared) = builder.doc.unwrap().pages.finish();
Ok((DocumentNode(pages).pack(), shared)) Ok((DocumentNode::new(pages.to_vec()).pack(), shared))
} }
/// Realize into a node that is capable of block-level layout. /// Realize into a node that is capable of block-level layout.
@ -185,7 +183,7 @@ fn realize_block<'a>(
builder.accept(content, styles)?; builder.accept(content, styles)?;
builder.interrupt_par()?; builder.interrupt_par()?;
let (children, shared) = builder.flow.0.finish(); let (children, shared) = builder.flow.0.finish();
Ok((FlowNode(children).pack(), shared)) Ok((FlowNode::new(children.to_vec()).pack(), shared))
} }
/// Builds a document or a flow node from content. /// Builds a document or a flow node from content.
@ -211,6 +209,7 @@ struct Scratch<'a> {
styles: Arena<StyleChain<'a>>, styles: Arena<StyleChain<'a>>,
/// An arena where intermediate content resulting from show rules is stored. /// An arena where intermediate content resulting from show rules is stored.
content: Arena<Content>, content: Arena<Content>,
maps: Arena<StyleMap>,
} }
impl<'a, 'v, 't> Builder<'a, 'v, 't> { impl<'a, 'v, 't> Builder<'a, 'v, 't> {
@ -231,10 +230,8 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
if content.has::<dyn LayoutMath>() && !content.is::<FormulaNode>() { if content.has::<dyn LayoutMath>() && !content.is::<FormulaNode>() {
content = self content =
.scratch self.scratch.content.alloc(FormulaNode::new(content.clone()).pack());
.content
.alloc(FormulaNode { body: content.clone(), block: false }.pack());
} }
// Prepare only if this is the first application for this node. // Prepare only if this is the first application for this node.
@ -252,8 +249,9 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
} }
if let Some(seq) = content.to::<SequenceNode>() { if let Some(seq) = content.to::<SequenceNode>() {
for sub in &seq.0 { for sub in seq.children() {
self.accept(sub, styles)?; let stored = self.scratch.content.alloc(sub);
self.accept(stored, styles)?;
} }
return Ok(()); return Ok(());
} }
@ -269,8 +267,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
self.interrupt_list()?; self.interrupt_list()?;
if content.is::<ListItem>() { if self.list.accept(content, styles) {
self.list.accept(content, styles);
return Ok(()); return Ok(());
} }
@ -286,7 +283,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
let keep = content let keep = content
.to::<PagebreakNode>() .to::<PagebreakNode>()
.map_or(false, |pagebreak| !pagebreak.weak); .map_or(false, |pagebreak| !pagebreak.weak());
self.interrupt_page(keep.then(|| styles))?; self.interrupt_page(keep.then(|| styles))?;
@ -308,11 +305,13 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
styled: &'a StyledNode, styled: &'a StyledNode,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
let map = self.scratch.maps.alloc(styled.map());
let stored = self.scratch.styles.alloc(styles); let stored = self.scratch.styles.alloc(styles);
let styles = stored.chain(&styled.map); let content = self.scratch.content.alloc(styled.sub());
self.interrupt_style(&styled.map, None)?; let styles = stored.chain(map);
self.accept(&styled.sub, styles)?; self.interrupt_style(&map, None)?;
self.interrupt_style(&styled.map, Some(styles))?; self.accept(content, styles)?;
self.interrupt_style(map, Some(styles))?;
Ok(()) Ok(())
} }
@ -381,7 +380,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
let (flow, shared) = mem::take(&mut self.flow).0.finish(); let (flow, shared) = mem::take(&mut self.flow).0.finish();
let styles = let styles =
if shared == StyleChain::default() { styles.unwrap() } else { shared }; if shared == StyleChain::default() { styles.unwrap() } else { shared };
let page = PageNode(FlowNode(flow).pack()).pack(); let page = PageNode::new(FlowNode::new(flow.to_vec()).pack()).pack();
let stored = self.scratch.content.alloc(page); let stored = self.scratch.content.alloc(page);
self.accept(stored, styles)?; self.accept(stored, styles)?;
} }
@ -392,7 +391,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
/// Accepts pagebreaks and pages. /// Accepts pagebreaks and pages.
struct DocBuilder<'a> { struct DocBuilder<'a> {
/// The page runs built so far. /// The page runs built so far.
pages: StyleVecBuilder<'a, PageNode>, pages: StyleVecBuilder<'a, Content>,
/// Whether to keep a following page even if it is empty. /// Whether to keep a following page even if it is empty.
keep_next: bool, keep_next: bool,
} }
@ -400,12 +399,12 @@ struct DocBuilder<'a> {
impl<'a> DocBuilder<'a> { impl<'a> DocBuilder<'a> {
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
if let Some(pagebreak) = content.to::<PagebreakNode>() { if let Some(pagebreak) = content.to::<PagebreakNode>() {
self.keep_next = !pagebreak.weak; self.keep_next = !pagebreak.weak();
return true; return true;
} }
if let Some(page) = content.to::<PageNode>() { if content.is::<PageNode>() {
self.pages.push(page.clone(), styles); self.pages.push(content.clone(), styles);
self.keep_next = false; self.keep_next = false;
return true; return true;
} }
@ -441,11 +440,11 @@ impl<'a> FlowBuilder<'a> {
if content.has::<dyn Layout>() || content.is::<ParNode>() { if content.has::<dyn Layout>() || content.is::<ParNode>() {
let is_tight_list = if let Some(node) = content.to::<ListNode>() { let is_tight_list = if let Some(node) = content.to::<ListNode>() {
node.tight node.tight()
} else if let Some(node) = content.to::<EnumNode>() { } else if let Some(node) = content.to::<EnumNode>() {
node.tight node.tight()
} else if let Some(node) = content.to::<TermsNode>() { } else if let Some(node) = content.to::<TermsNode>() {
node.tight node.tight()
} else { } else {
false false
}; };
@ -458,9 +457,9 @@ impl<'a> FlowBuilder<'a> {
let above = styles.get(BlockNode::ABOVE); let above = styles.get(BlockNode::ABOVE);
let below = styles.get(BlockNode::BELOW); let below = styles.get(BlockNode::BELOW);
self.0.push(above.pack(), styles); self.0.push(above.clone().pack(), styles);
self.0.push(content.clone(), styles); self.0.push(content.clone(), styles);
self.0.push(below.pack(), styles); self.0.push(below.clone().pack(), styles);
return true; return true;
} }
@ -479,7 +478,7 @@ impl<'a> ParBuilder<'a> {
|| content.is::<HNode>() || content.is::<HNode>()
|| content.is::<LinebreakNode>() || content.is::<LinebreakNode>()
|| content.is::<SmartQuoteNode>() || content.is::<SmartQuoteNode>()
|| content.to::<FormulaNode>().map_or(false, |node| !node.block) || content.to::<FormulaNode>().map_or(false, |node| !node.block())
|| content.is::<BoxNode>() || content.is::<BoxNode>()
{ {
self.0.push(content.clone(), styles); self.0.push(content.clone(), styles);
@ -491,14 +490,14 @@ impl<'a> ParBuilder<'a> {
fn finish(self) -> (Content, StyleChain<'a>) { fn finish(self) -> (Content, StyleChain<'a>) {
let (children, shared) = self.0.finish(); let (children, shared) = self.0.finish();
(ParNode(children).pack(), shared) (ParNode::new(children.to_vec()).pack(), shared)
} }
} }
/// Accepts list / enum items, spaces, paragraph breaks. /// Accepts list / enum items, spaces, paragraph breaks.
struct ListBuilder<'a> { struct ListBuilder<'a> {
/// The list items collected so far. /// The list items collected so far.
items: StyleVecBuilder<'a, ListItem>, items: StyleVecBuilder<'a, Content>,
/// Whether the list contains no paragraph breaks. /// Whether the list contains no paragraph breaks.
tight: bool, tight: bool,
/// Trailing content for which it is unclear whether it is part of the list. /// Trailing content for which it is unclear whether it is part of the list.
@ -514,15 +513,19 @@ impl<'a> ListBuilder<'a> {
return true; return true;
} }
if let Some(item) = content.to::<ListItem>() { if (content.is::<ListItem>()
if self.items.items().next().map_or(true, |first| { || content.is::<EnumItem>()
std::mem::discriminant(item) == std::mem::discriminant(first) || content.is::<TermItem>())
}) { && self
self.items.push(item.clone(), styles); .items
.items()
.next()
.map_or(true, |first| first.id() == content.id())
{
self.items.push(content.clone(), styles);
self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>()); self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
return true; return true;
} }
}
false false
} }
@ -530,31 +533,48 @@ impl<'a> ListBuilder<'a> {
fn finish(self) -> (Content, StyleChain<'a>) { fn finish(self) -> (Content, StyleChain<'a>) {
let (items, shared) = self.items.finish(); let (items, shared) = self.items.finish();
let item = items.items().next().unwrap(); let item = items.items().next().unwrap();
let output = match item { let output = if item.is::<ListItem>() {
ListItem::List(_) => ListNode { ListNode::new(
tight: self.tight, items
items: items.map(|item| match item { .iter()
ListItem::List(item) => item.clone(), .map(|(item, map)| {
_ => panic!("wrong list item"), let item = item.to::<ListItem>().unwrap();
}), ListItem::new(item.body().styled_with_map(map.clone()))
} })
.pack(), .collect::<Vec<_>>(),
ListItem::Enum(..) => EnumNode { )
tight: self.tight, .with_tight(self.tight)
items: items.map(|item| match item { .pack()
ListItem::Enum(number, body) => (*number, body.clone()), } else if item.is::<EnumItem>() {
_ => panic!("wrong list item"), EnumNode::new(
}), items
} .iter()
.pack(), .map(|(item, map)| {
ListItem::Term(_) => TermsNode { let item = item.to::<EnumItem>().unwrap();
tight: self.tight, EnumItem::new(item.body().styled_with_map(map.clone()))
items: items.map(|item| match item { .with_number(item.number())
ListItem::Term(item) => item.clone(), })
_ => panic!("wrong list item"), .collect::<Vec<_>>(),
}), )
} .with_tight(self.tight)
.pack(), .pack()
} else if item.is::<TermItem>() {
TermsNode::new(
items
.iter()
.map(|(item, map)| {
let item = item.to::<TermItem>().unwrap();
TermItem::new(
item.term().styled_with_map(map.clone()),
item.description().styled_with_map(map.clone()),
)
})
.collect::<Vec<_>>(),
)
.with_tight(self.tight)
.pack()
} else {
unreachable!()
}; };
(output, shared) (output, shared)
} }
@ -569,18 +589,3 @@ impl Default for ListBuilder<'_> {
} }
} }
} }
/// An item in a list.
#[capable]
#[derive(Debug, Clone, Hash)]
pub enum ListItem {
/// An item of a bullet list.
List(Content),
/// An item of a numbered list.
Enum(Option<NonZeroUsize>, Content),
/// An item of a term list.
Term(TermItem),
}
#[node]
impl ListItem {}

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Padding
/// Add spacing around content. /// Add spacing around content.
/// ///
/// The `pad` function adds spacing around content. The spacing can be specified /// The `pad` function adds spacing around content. The spacing can be specified
@ -17,21 +16,6 @@ use crate::prelude::*;
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
/// - body: `Content` (positional, required)
/// The content to pad at the sides.
///
/// - left: `Rel<Length>` (named)
/// The padding at the left side.
///
/// - right: `Rel<Length>` (named)
/// The padding at the right side.
///
/// - top: `Rel<Length>` (named)
/// The padding at the top side.
///
/// - bottom: `Rel<Length>` (named)
/// The padding at the bottom side.
///
/// - x: `Rel<Length>` (named) /// - x: `Rel<Length>` (named)
/// The horizontal padding. Both `left` and `right` take precedence over this. /// The horizontal padding. Both `left` and `right` take precedence over this.
/// ///
@ -41,20 +25,37 @@ use crate::prelude::*;
/// - rest: `Rel<Length>` (named) /// - rest: `Rel<Length>` (named)
/// The padding for all sides. All other parameters take precedence over this. /// The padding for all sides. All other parameters take precedence over this.
/// ///
/// ## Category /// Display: Padding
/// layout /// Category: layout
#[func] #[node(Construct, Layout)]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct PadNode { pub struct PadNode {
/// The amount of padding. /// The content to pad at the sides.
pub padding: Sides<Rel<Length>>, #[positional]
/// The content whose sides to pad. #[required]
pub body: Content, pub body: Content,
/// The padding at the left side.
#[named]
#[default]
pub left: Rel<Length>,
/// The padding at the right side.
#[named]
#[default]
pub right: Rel<Length>,
/// The padding at the top side.
#[named]
#[default]
pub top: Rel<Length>,
/// The padding at the bottom side.
#[named]
#[default]
pub bottom: Rel<Length>,
} }
#[node] impl Construct for PadNode {
impl PadNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let all = args.named("rest")?.or(args.find()?); let all = args.named("rest")?.or(args.find()?);
let x = args.named("x")?; let x = args.named("x")?;
@ -64,8 +65,12 @@ impl PadNode {
let right = args.named("right")?.or(x).or(all).unwrap_or_default(); let right = args.named("right")?.or(x).or(all).unwrap_or_default();
let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default();
let body = args.expect::<Content>("body")?; let body = args.expect::<Content>("body")?;
let padding = Sides::new(left, top, right, bottom); Ok(Self::new(body)
Ok(Self { padding, body }.pack()) .with_left(left)
.with_top(top)
.with_bottom(bottom)
.with_right(right)
.pack())
} }
} }
@ -79,9 +84,10 @@ impl Layout for PadNode {
let mut backlog = vec![]; let mut backlog = vec![];
// Layout child into padded regions. // Layout child into padded regions.
let padding = self.padding.resolve(styles); let sides = Sides::new(self.left(), self.top(), self.right(), self.bottom());
let padding = sides.resolve(styles);
let pod = regions.map(&mut backlog, |size| shrink(size, padding)); let pod = regions.map(&mut backlog, |size| shrink(size, padding));
let mut fragment = self.body.layout(vt, styles, pod)?; let mut fragment = self.body().layout(vt, styles, pod)?;
for frame in &mut fragment { for frame in &mut fragment {
// Apply the padding inversely such that the grown size padded // Apply the padding inversely such that the grown size padded

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use super::ColumnsNode; use super::ColumnsNode;
use crate::prelude::*; use crate::prelude::*;
/// # Page
/// Layouts its child onto one or multiple pages. /// Layouts its child onto one or multiple pages.
/// ///
/// Although this function is primarily used in set rules to affect page /// Although this function is primarily used in set rules to affect page
@ -14,13 +13,6 @@ use crate::prelude::*;
/// the pages will grow to fit their content on the respective axis. /// the pages will grow to fit their content on the respective axis.
/// ///
/// ## Parameters /// ## Parameters
/// - body: `Content` (positional, required)
/// The contents of the page(s).
///
/// Multiple pages will be created if the content does not fit on a single
/// page. A new page with the page properties prior to the function invocation
/// will be created after the body has been typeset.
///
/// - paper: `Paper` (positional, settable) /// - paper: `Paper` (positional, settable)
/// A standard paper size to set width and height. When this is not specified, /// A standard paper size to set width and height. When this is not specified,
/// Typst defaults to `{"a4"}` paper. /// Typst defaults to `{"a4"}` paper.
@ -33,15 +25,25 @@ use crate::prelude::*;
/// There you go, US friends! /// There you go, US friends!
/// ``` /// ```
/// ///
/// ## Category /// Display: Page
/// layout /// Category: layout
#[func]
#[capable]
#[derive(Clone, Hash)]
pub struct PageNode(pub Content);
#[node] #[node]
impl PageNode { #[set({
if let Some(paper) = args.named_or_find::<Paper>("paper")? {
styles.set(Self::WIDTH, Smart::Custom(paper.width().into()));
styles.set(Self::HEIGHT, Smart::Custom(paper.height().into()));
}
})]
pub struct PageNode {
/// The contents of the page(s).
///
/// Multiple pages will be created if the content does not fit on a single
/// page. A new page with the page properties prior to the function invocation
/// will be created after the body has been typeset.
#[positional]
#[required]
pub body: Content,
/// The width of the page. /// The width of the page.
/// ///
/// ```example /// ```example
@ -54,8 +56,10 @@ impl PageNode {
/// box(square(width: 1cm)) /// box(square(width: 1cm))
/// } /// }
/// ``` /// ```
#[property(resolve)] #[settable]
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width().into()); #[resolve]
#[default(Smart::Custom(Paper::A4.width().into()))]
pub width: Smart<Length>,
/// The height of the page. /// The height of the page.
/// ///
@ -63,8 +67,10 @@ impl PageNode {
/// by inserting a [page break]($func/pagebreak). Most examples throughout /// by inserting a [page break]($func/pagebreak). Most examples throughout
/// this documentation use `{auto}` for the height of the page to /// this documentation use `{auto}` for the height of the page to
/// dynamically grow and shrink to fit their content. /// dynamically grow and shrink to fit their content.
#[property(resolve)] #[settable]
pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height().into()); #[resolve]
#[default(Smart::Custom(Paper::A4.height().into()))]
pub height: Smart<Length>,
/// Whether the page is flipped into landscape orientation. /// Whether the page is flipped into landscape orientation.
/// ///
@ -84,7 +90,9 @@ impl PageNode {
/// New York, NY 10001 \ /// New York, NY 10001 \
/// +1 555 555 5555 /// +1 555 555 5555
/// ``` /// ```
pub const FLIPPED: bool = false; #[settable]
#[default(false)]
pub flipped: bool,
/// The page's margins. /// The page's margins.
/// ///
@ -114,8 +122,10 @@ impl PageNode {
/// fill: aqua, /// fill: aqua,
/// ) /// )
/// ``` /// ```
#[property(fold)] #[settable]
pub const MARGIN: Sides<Option<Smart<Rel<Length>>>> = Sides::splat(Smart::Auto); #[fold]
#[default]
pub margin: Sides<Option<Smart<Rel<Length>>>>,
/// How many columns the page has. /// How many columns the page has.
/// ///
@ -131,7 +141,9 @@ impl PageNode {
/// emissions and mitigate the impacts /// emissions and mitigate the impacts
/// of a rapidly changing climate. /// of a rapidly changing climate.
/// ``` /// ```
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); #[settable]
#[default(NonZeroUsize::new(1).unwrap())]
pub columns: NonZeroUsize,
/// The page's background color. /// The page's background color.
/// ///
@ -145,7 +157,9 @@ impl PageNode {
/// #set text(fill: rgb("fdfdfd")) /// #set text(fill: rgb("fdfdfd"))
/// *Dark mode enabled.* /// *Dark mode enabled.*
/// ``` /// ```
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// The page's header. /// The page's header.
/// ///
@ -166,8 +180,9 @@ impl PageNode {
/// ///
/// #lorem(18) /// #lorem(18)
/// ``` /// ```
#[property(referenced)] #[settable]
pub const HEADER: Option<Marginal> = None; #[default]
pub header: Option<Marginal>,
/// The page's footer. /// The page's footer.
/// ///
@ -190,8 +205,9 @@ impl PageNode {
/// ///
/// #lorem(18) /// #lorem(18)
/// ``` /// ```
#[property(referenced)] #[settable]
pub const FOOTER: Option<Marginal> = None; #[default]
pub footer: Option<Marginal>,
/// Content in the page's background. /// Content in the page's background.
/// ///
@ -211,8 +227,9 @@ impl PageNode {
/// In the year 2023, we plan to take over the world /// In the year 2023, we plan to take over the world
/// (of typesetting). /// (of typesetting).
/// ``` /// ```
#[property(referenced)] #[settable]
pub const BACKGROUND: Option<Marginal> = None; #[default]
pub background: Option<Marginal>,
/// Content in the page's foreground. /// Content in the page's foreground.
/// ///
@ -228,26 +245,9 @@ impl PageNode {
/// "Weak Reject" because they did /// "Weak Reject" because they did
/// not understand our approach... /// not understand our approach...
/// ``` /// ```
#[property(referenced)] #[settable]
pub const FOREGROUND: Option<Marginal> = None; #[default]
pub foreground: Option<Marginal>,
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
fn set(...) {
if let Some(paper) = args.named_or_find::<Paper>("paper")? {
styles.set(Self::WIDTH, Smart::Custom(paper.width().into()));
styles.set(Self::HEIGHT, Smart::Custom(paper.height().into()));
}
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl PageNode { impl PageNode {
@ -276,26 +276,22 @@ impl PageNode {
let default = Rel::from(0.1190 * min); let default = Rel::from(0.1190 * min);
let padding = styles.get(Self::MARGIN).map(|side| side.unwrap_or(default)); let padding = styles.get(Self::MARGIN).map(|side| side.unwrap_or(default));
let mut child = self.0.clone(); let mut child = self.body();
// Realize columns. // Realize columns.
let columns = styles.get(Self::COLUMNS); let columns = styles.get(Self::COLUMNS);
if columns.get() > 1 { if columns.get() > 1 {
child = ColumnsNode { count: columns, body: self.0.clone() }.pack(); child = ColumnsNode::new(columns, child).pack();
} }
// Realize margins. // Realize margins.
child = child.padded(padding); child = child.padded(padding);
// Realize background fill.
if let Some(fill) = styles.get(Self::FILL) {
child = child.filled(fill);
}
// Layout the child. // Layout the child.
let regions = Regions::repeat(size, size.map(Abs::is_finite)); let regions = Regions::repeat(size, size.map(Abs::is_finite));
let mut fragment = child.layout(vt, styles, regions)?; let mut fragment = child.layout(vt, styles, regions)?;
let fill = styles.get(Self::FILL);
let header = styles.get(Self::HEADER); let header = styles.get(Self::HEADER);
let footer = styles.get(Self::FOOTER); let footer = styles.get(Self::FOOTER);
let foreground = styles.get(Self::FOREGROUND); let foreground = styles.get(Self::FOREGROUND);
@ -303,17 +299,21 @@ impl PageNode {
// Realize overlays. // Realize overlays.
for frame in &mut fragment { for frame in &mut fragment {
if let Some(fill) = fill {
frame.fill(fill);
}
let size = frame.size(); let size = frame.size();
let pad = padding.resolve(styles).relative_to(size); let pad = padding.resolve(styles).relative_to(size);
let pw = size.x - pad.left - pad.right; let pw = size.x - pad.left - pad.right;
let py = size.y - pad.bottom; let py = size.y - pad.bottom;
for (marginal, pos, area) in [ for (marginal, pos, area) in [
(header, Point::with_x(pad.left), Size::new(pw, pad.top)), (&header, Point::with_x(pad.left), Size::new(pw, pad.top)),
(footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), (&footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)),
(foreground, Point::zero(), size), (&foreground, Point::zero(), size),
(background, Point::zero(), size), (&background, Point::zero(), size),
] { ] {
let in_background = std::ptr::eq(marginal, background); let in_background = std::ptr::eq(marginal, &background);
let Some(marginal) = marginal else { continue }; let Some(marginal) = marginal else { continue };
let content = marginal.resolve(vt, page)?; let content = marginal.resolve(vt, page)?;
let pod = Regions::one(area, Axes::splat(true)); let pod = Regions::one(area, Axes::splat(true));
@ -332,15 +332,6 @@ impl PageNode {
} }
} }
impl Debug for PageNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Page(")?;
self.0.fmt(f)?;
f.write_str(")")
}
}
/// # Page Break
/// A manual page break. /// A manual page break.
/// ///
/// Must not be used inside any containers. /// Must not be used inside any containers.
@ -355,26 +346,15 @@ impl Debug for PageNode {
/// In 1984, the first ... /// In 1984, the first ...
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Page Break
/// - weak: `bool` (named) /// Category: layout
/// If `{true}`, the page break is skipped if the current page is already
/// empty.
///
/// ## Category
/// layout
#[func]
#[capable]
#[derive(Debug, Copy, Clone, Hash)]
pub struct PagebreakNode {
pub weak: bool,
}
#[node] #[node]
impl PagebreakNode { pub struct PagebreakNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { /// If `{true}`, the page break is skipped if the current page is already
let weak = args.named("weak")?.unwrap_or(false); /// empty.
Ok(Self { weak }.pack()) #[named]
} #[default(false)]
pub weak: bool,
} }
/// A header, footer, foreground or background definition. /// A header, footer, foreground or background definition.
@ -399,12 +379,19 @@ impl Marginal {
} }
} }
castable! { cast_from_value! {
Marginal, Marginal,
v: Content => Self::Content(v), v: Content => Self::Content(v),
v: Func => Self::Func(v), v: Func => Self::Func(v),
} }
cast_to_value! {
v: Marginal => match v {
Marginal::Content(v) => v.into(),
Marginal::Func(v) => v.into(),
}
}
/// Specification of a paper. /// Specification of a paper.
#[derive(Debug, Copy, Clone, Hash)] #[derive(Debug, Copy, Clone, Hash)]
pub struct Paper { pub struct Paper {
@ -450,7 +437,7 @@ macro_rules! papers {
} }
} }
castable! { cast_from_value! {
Paper, Paper,
$( $(
/// Produces a paper of the respective size. /// Produces a paper of the respective size.

View File

@ -2,7 +2,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use typst::model::Key; use typst::model::{Key, StyledNode};
use super::{BoxNode, HNode, Sizing, Spacing}; use super::{BoxNode, HNode, Sizing, Spacing};
use crate::layout::AlignNode; use crate::layout::AlignNode;
@ -12,7 +12,6 @@ use crate::text::{
shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode, shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode,
}; };
/// # Paragraph
/// Arrange text, spacing and inline-level nodes into a paragraph. /// Arrange text, spacing and inline-level nodes into a paragraph.
/// ///
/// Although this function is primarily used in set rules to affect paragraph /// Although this function is primarily used in set rules to affect paragraph
@ -40,15 +39,14 @@ use crate::text::{
/// - body: `Content` (positional, required) /// - body: `Content` (positional, required)
/// The contents of the paragraph. /// The contents of the paragraph.
/// ///
/// ## Category /// Display: Paragraph
/// layout /// Category: layout
#[func] #[node(Construct)]
#[capable] pub struct ParNode {
#[derive(Hash)] /// The paragraph's children.
pub struct ParNode(pub StyleVec<Content>); #[variadic]
pub children: Vec<Content>,
#[node]
impl ParNode {
/// The indent the first line of a consecutive paragraph should have. /// The indent the first line of a consecutive paragraph should have.
/// ///
/// The first paragraph on a page will never be indented. /// The first paragraph on a page will never be indented.
@ -57,14 +55,18 @@ impl ParNode {
/// space between paragraphs or indented first lines. Consider turning the /// space between paragraphs or indented first lines. Consider turning the
/// [paragraph spacing]($func/block.spacing) off when using this property /// [paragraph spacing]($func/block.spacing) off when using this property
/// (e.g. using `[#show par: set block(spacing: 0pt)]`). /// (e.g. using `[#show par: set block(spacing: 0pt)]`).
#[property(resolve)] #[settable]
pub const INDENT: Length = Length::zero(); #[resolve]
#[default]
pub indent: Length,
/// The spacing between lines. /// The spacing between lines.
/// ///
/// The default value is `{0.65em}`. /// The default value is `{0.65em}`.
#[property(resolve)] #[settable]
pub const LEADING: Length = Em::new(0.65).into(); #[resolve]
#[default(Em::new(0.65).into())]
pub leading: Length,
/// Whether to justify text in its line. /// Whether to justify text in its line.
/// ///
@ -75,7 +77,9 @@ impl ParNode {
/// Note that the current [alignment]($func/align) still has an effect on /// 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 /// the placement of the last line except if it ends with a [justified line
/// break]($func/linebreak.justify). /// break]($func/linebreak.justify).
pub const JUSTIFY: bool = false; #[settable]
#[default(false)]
pub justify: bool,
/// How to determine line breaks. /// How to determine line breaks.
/// ///
@ -100,16 +104,20 @@ impl ParNode {
/// very aesthetic example is one /// very aesthetic example is one
/// of them. /// of them.
/// ``` /// ```
pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto; #[settable]
#[default]
pub linebreaks: Smart<Linebreaks>,
}
impl Construct for ParNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
// node. Instead, it just ensures that the passed content lives in a // node. Instead, it just ensures that the passed content lives in a
// separate paragraph and styles it. // separate paragraph and styles it.
Ok(Content::sequence(vec![ Ok(Content::sequence(vec![
ParbreakNode.pack(), ParbreakNode::new().pack(),
args.expect("body")?, args.expect("body")?,
ParbreakNode.pack(), ParbreakNode::new().pack(),
])) ]))
} }
} }
@ -136,14 +144,15 @@ impl ParNode {
expand: bool, expand: bool,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut vt = Vt { world, provider, introspector }; let mut vt = Vt { world, provider, introspector };
let children = par.children();
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments) = collect(par, &styles, consecutive); let (text, segments) = collect(&children, &styles, consecutive)?;
// Perform BiDi analysis and then prepare paragraph layout by building a // Perform BiDi analysis and then prepare paragraph layout by building a
// representation on which we can do line breaking without layouting // representation on which we can do line breaking without layouting
// each and every line from scratch. // each and every line from scratch.
let p = prepare(&mut vt, par, &text, segments, styles, region)?; let p = prepare(&mut vt, &children, &text, segments, styles, region)?;
// Break the paragraph into lines. // Break the paragraph into lines.
let lines = linebreak(&vt, &p, region.x); let lines = linebreak(&vt, &p, region.x);
@ -165,18 +174,11 @@ impl ParNode {
} }
} }
impl Debug for ParNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Par ")?;
self.0.fmt(f)
}
}
/// A horizontal alignment. /// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub GenAlign); pub struct HorizontalAlign(pub GenAlign);
castable! { cast_from_value! {
HorizontalAlign, HorizontalAlign,
align: GenAlign => match align.axis() { align: GenAlign => match align.axis() {
Axis::X => Self(align), Axis::X => Self(align),
@ -201,7 +203,7 @@ pub enum Linebreaks {
Optimized, Optimized,
} }
castable! { cast_from_value! {
Linebreaks, Linebreaks,
/// Determine the line breaks in a simple first-fit style. /// Determine the line breaks in a simple first-fit style.
"simple" => Self::Simple, "simple" => Self::Simple,
@ -212,7 +214,13 @@ castable! {
"optimized" => Self::Optimized, "optimized" => Self::Optimized,
} }
/// # Paragraph Break cast_to_value! {
v: Linebreaks => Value::from(match v {
Linebreaks::Simple => "simple",
Linebreaks::Optimized => "optimized",
})
}
/// A paragraph break. /// A paragraph break.
/// ///
/// This starts a new paragraph. Especially useful when used within code like /// This starts a new paragraph. Especially useful when used within code like
@ -232,19 +240,10 @@ castable! {
/// Instead of calling this function, you can insert a blank line into your /// Instead of calling this function, you can insert a blank line into your
/// markup to create a paragraph break. /// markup to create a paragraph break.
/// ///
/// ## Category /// Display: Paragraph Break
/// layout /// Category: layout
#[func] #[node(Unlabellable)]
#[capable(Unlabellable)] pub struct ParbreakNode {}
#[derive(Debug, Hash)]
pub struct ParbreakNode;
#[node]
impl ParbreakNode {
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
Ok(Self.pack())
}
}
impl Unlabellable for ParbreakNode {} impl Unlabellable for ParbreakNode {}
@ -343,7 +342,7 @@ impl Segment<'_> {
match *self { match *self {
Self::Text(len) => len, Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Box(node) if node.width.is_fractional() => SPACING_REPLACE.len_utf8(), Self::Box(node) if node.width().is_fractional() => SPACING_REPLACE.len_utf8(),
Self::Formula(_) | Self::Box(_) => NODE_REPLACE.len_utf8(), Self::Formula(_) | Self::Box(_) => NODE_REPLACE.len_utf8(),
} }
} }
@ -485,21 +484,20 @@ impl<'a> Line<'a> {
/// Collect all text of the paragraph into one string. This also performs /// Collect all text of the paragraph into one string. This also performs
/// string-level preprocessing like case transformations. /// string-level preprocessing like case transformations.
fn collect<'a>( fn collect<'a>(
par: &'a ParNode, children: &'a [Content],
styles: &'a StyleChain<'a>, styles: &'a StyleChain<'a>,
consecutive: bool, consecutive: bool,
) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { ) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> {
let mut full = String::new(); let mut full = String::new();
let mut quoter = Quoter::new(); let mut quoter = Quoter::new();
let mut segments = vec![]; let mut segments = vec![];
let mut iter = par.0.iter().peekable(); let mut iter = children.iter().peekable();
if consecutive { if consecutive {
let indent = styles.get(ParNode::INDENT); let indent = styles.get(ParNode::INDENT);
if !indent.is_zero() if !indent.is_zero()
&& par && children
.0 .iter()
.items()
.find_map(|child| { .find_map(|child| {
if child.with::<dyn Behave>().map_or(false, |behaved| { if child.with::<dyn Behave>().map_or(false, |behaved| {
behaved.behaviour() == Behaviour::Ignorant behaved.behaviour() == Behaviour::Ignorant
@ -518,24 +516,30 @@ fn collect<'a>(
} }
} }
while let Some((child, map)) = iter.next() { while let Some(mut child) = iter.next() {
let styles = styles.chain(map); let outer = styles;
let mut styles = *styles;
if let Some(node) = child.to::<StyledNode>() {
child = Box::leak(Box::new(node.sub()));
styles = outer.chain(Box::leak(Box::new(node.map())));
}
let segment = if child.is::<SpaceNode>() { let segment = if child.is::<SpaceNode>() {
full.push(' '); full.push(' ');
Segment::Text(1) Segment::Text(1)
} else if let Some(node) = child.to::<TextNode>() { } else if let Some(node) = child.to::<TextNode>() {
let prev = full.len(); let prev = full.len();
if let Some(case) = styles.get(TextNode::CASE) { if let Some(case) = styles.get(TextNode::CASE) {
full.push_str(&case.apply(&node.0)); full.push_str(&case.apply(&node.text()));
} else { } else {
full.push_str(&node.0); full.push_str(&node.text());
} }
Segment::Text(full.len() - prev) Segment::Text(full.len() - prev)
} else if let Some(&node) = child.to::<HNode>() { } else if let Some(node) = child.to::<HNode>() {
full.push(SPACING_REPLACE); full.push(SPACING_REPLACE);
Segment::Spacing(node.amount) Segment::Spacing(node.amount())
} else if let Some(node) = child.to::<LinebreakNode>() { } else if let Some(node) = child.to::<LinebreakNode>() {
let c = if node.justify { '\u{2028}' } else { '\n' }; let c = if node.justify() { '\u{2028}' } else { '\n' };
full.push(c); full.push(c);
Segment::Text(c.len_utf8()) Segment::Text(c.len_utf8())
} else if let Some(node) = child.to::<SmartQuoteNode>() { } else if let Some(node) = child.to::<SmartQuoteNode>() {
@ -544,9 +548,9 @@ fn collect<'a>(
let lang = styles.get(TextNode::LANG); let lang = styles.get(TextNode::LANG);
let region = styles.get(TextNode::REGION); let region = styles.get(TextNode::REGION);
let quotes = Quotes::from_lang(lang, region); let quotes = Quotes::from_lang(lang, region);
let peeked = iter.peek().and_then(|(child, _)| { let peeked = iter.peek().and_then(|child| {
if let Some(node) = child.to::<TextNode>() { if let Some(node) = child.to::<TextNode>() {
node.0.chars().next() node.text().chars().next()
} else if child.is::<SmartQuoteNode>() { } else if child.is::<SmartQuoteNode>() {
Some('"') Some('"')
} else if child.is::<SpaceNode>() || child.is::<HNode>() { } else if child.is::<SpaceNode>() || child.is::<HNode>() {
@ -556,23 +560,25 @@ fn collect<'a>(
} }
}); });
full.push_str(quoter.quote(&quotes, node.double, peeked)); full.push_str(quoter.quote(&quotes, node.double(), peeked));
} else { } else {
full.push(if node.double { '"' } else { '\'' }); full.push(if node.double() { '"' } else { '\'' });
} }
Segment::Text(full.len() - prev) Segment::Text(full.len() - prev)
} else if let Some(node) = child.to::<FormulaNode>() { } else if let Some(node) = child.to::<FormulaNode>() {
full.push(NODE_REPLACE); full.push(NODE_REPLACE);
Segment::Formula(node) Segment::Formula(node)
} else if let Some(node) = child.to::<BoxNode>() { } else if let Some(node) = child.to::<BoxNode>() {
full.push(if node.width.is_fractional() { full.push(if node.width().is_fractional() {
SPACING_REPLACE SPACING_REPLACE
} else { } else {
NODE_REPLACE NODE_REPLACE
}); });
Segment::Box(node) Segment::Box(node)
} else if let Some(span) = child.span() {
bail!(span, "unexpected document child");
} else { } else {
panic!("unexpected par child: {child:?}"); continue;
}; };
if let Some(last) = full.chars().last() { if let Some(last) = full.chars().last() {
@ -591,14 +597,14 @@ fn collect<'a>(
segments.push((segment, styles)); segments.push((segment, styles));
} }
(full, segments) Ok((full, segments))
} }
/// Prepare paragraph layout by shaping the whole paragraph and layouting all /// Prepare paragraph layout by shaping the whole paragraph and layouting all
/// contained inline-level content. /// contained inline-level content.
fn prepare<'a>( fn prepare<'a>(
vt: &mut Vt, vt: &mut Vt,
par: &'a ParNode, children: &'a [Content],
text: &'a str, text: &'a str,
segments: Vec<(Segment<'a>, StyleChain<'a>)>, segments: Vec<(Segment<'a>, StyleChain<'a>)>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
@ -639,7 +645,7 @@ fn prepare<'a>(
items.push(Item::Frame(frame)); items.push(Item::Frame(frame));
} }
Segment::Box(node) => { Segment::Box(node) => {
if let Sizing::Fr(v) = node.width { if let Sizing::Fr(v) = node.width() {
items.push(Item::Fractional(v, Some((node, styles)))); items.push(Item::Fractional(v, Some((node, styles))));
} else { } else {
let pod = Regions::one(region, Axes::splat(false)); let pod = Regions::one(region, Axes::splat(false));
@ -657,9 +663,9 @@ fn prepare<'a>(
bidi, bidi,
items, items,
styles, styles,
hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE), hyphenate: shared_get(styles, children, TextNode::HYPHENATE),
lang: shared_get(styles, &par.0, TextNode::LANG), lang: shared_get(styles, children, TextNode::LANG),
align: styles.get(AlignNode::ALIGNS).x.resolve(styles), align: styles.get(AlignNode::ALIGNMENT).x.resolve(styles),
justify: styles.get(ParNode::JUSTIFY), justify: styles.get(ParNode::JUSTIFY),
}) })
} }
@ -722,12 +728,13 @@ fn is_compatible(a: Script, b: Script) -> bool {
/// paragraph. /// paragraph.
fn shared_get<'a, K: Key>( fn shared_get<'a, K: Key>(
styles: StyleChain<'a>, styles: StyleChain<'a>,
children: &StyleVec<Content>, children: &[Content],
key: K, key: K,
) -> Option<K::Output<'a>> { ) -> Option<K::Output> {
children children
.styles() .iter()
.all(|map| !map.contains(key)) .filter_map(|child| child.to::<StyledNode>())
.all(|node| !node.map().contains(key))
.then(|| styles.get(key)) .then(|| styles.get(key))
} }

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Place
/// Place content at an absolute position. /// Place content at an absolute position.
/// ///
/// Placed content will not affect the position of other content. Place is /// Placed content will not affect the position of other content. Place is
@ -22,48 +21,41 @@ use crate::prelude::*;
/// ) /// )
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Place
/// - alignment: `Axes<Option<GenAlign>>` (positional) /// Category: layout
/// Relative to which position in the parent container to place the content. #[node(Layout, Behave)]
/// pub struct PlaceNode {
/// When an axis of the page is `{auto}` sized, all alignments relative to that /// Relative to which position in the parent container to place the content.
/// axis will be ignored, instead, the item will be placed in the origin of the ///
/// axis. /// When an axis of the page is `{auto}` sized, all alignments relative to that
/// /// axis will be ignored, instead, the item will be placed in the origin of the
/// - body: `Content` (positional, required) /// axis.
/// The content to place. #[positional]
/// #[default(Axes::with_x(Some(GenAlign::Start)))]
/// - dx: `Rel<Length>` (named) pub alignment: Axes<Option<GenAlign>>,
/// The horizontal displacement of the placed content.
///
/// ```example
/// #set page(height: 100pt)
/// #for i in range(16) {
/// let amount = i * 4pt
/// place(center, dx: amount - 32pt, dy: amount)[A]
/// }
/// ```
///
/// - dy: `Rel<Length>` (named)
/// The vertical displacement of the placed content.
///
/// ## Category
/// layout
#[func]
#[capable(Layout, Behave)]
#[derive(Debug, Hash)]
pub struct PlaceNode(pub Content, bool);
#[node] /// The content to place.
impl PlaceNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[required]
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); pub body: Content,
let dx = args.named("dx")?.unwrap_or_default();
let dy = args.named("dy")?.unwrap_or_default(); /// The horizontal displacement of the placed content.
let body = args.expect::<Content>("body")?; ///
let out_of_flow = aligns.y.is_some(); /// ```example
Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns), out_of_flow).pack()) /// #set page(height: 100pt)
} /// #for i in range(16) {
/// let amount = i * 4pt
/// place(center, dx: amount - 32pt, dy: amount)[A]
/// }
/// ```
#[named]
#[default]
pub dx: Rel<Length>,
/// The vertical displacement of the placed content.
#[named]
#[default]
pub dy: Rel<Length>,
} }
impl Layout for PlaceNode { impl Layout for PlaceNode {
@ -83,7 +75,12 @@ impl Layout for PlaceNode {
Regions::one(regions.base(), expand) Regions::one(regions.base(), expand)
}; };
let mut frame = self.0.layout(vt, styles, pod)?.into_frame(); let child = self
.body()
.moved(Axes::new(self.dx(), self.dy()))
.aligned(self.alignment());
let mut frame = child.layout(vt, styles, pod)?.into_frame();
// If expansion is off, zero all sizes so that we don't take up any // If expansion is off, zero all sizes so that we don't take up any
// space in our parent. Otherwise, respect the expand settings. // space in our parent. Otherwise, respect the expand settings.
@ -99,7 +96,7 @@ impl PlaceNode {
/// origin. Instead of relative to the parent's current flow/cursor /// origin. Instead of relative to the parent's current flow/cursor
/// position. /// position.
pub fn out_of_flow(&self) -> bool { pub fn out_of_flow(&self) -> bool {
self.1 self.alignment().y.is_some()
} }
} }

View File

@ -2,7 +2,6 @@ use crate::prelude::*;
use super::AlignNode; use super::AlignNode;
/// # Repeat
/// Repeats content to the available space. /// Repeats content to the available space.
/// ///
/// This can be useful when implementing a custom index, reference, or outline. /// This can be useful when implementing a custom index, reference, or outline.
@ -22,22 +21,14 @@ use super::AlignNode;
/// ] /// ]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Repeat
/// - body: `Content` (positional, required) /// Category: layout
/// The content to repeat. #[node(Layout)]
/// pub struct RepeatNode {
/// ## Category /// The content to repeat.
/// layout #[positional]
#[func] #[required]
#[capable(Layout)] pub body: Content,
#[derive(Debug, Hash)]
pub struct RepeatNode(pub Content);
#[node]
impl RepeatNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl Layout for RepeatNode { impl Layout for RepeatNode {
@ -48,8 +39,8 @@ impl Layout for RepeatNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pod = Regions::one(regions.size, Axes::new(false, false)); let pod = Regions::one(regions.size, Axes::new(false, false));
let piece = self.0.layout(vt, styles, pod)?.into_frame(); let piece = self.body().layout(vt, styles, pod)?.into_frame();
let align = styles.get(AlignNode::ALIGNS).x.resolve(styles); let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles);
let fill = regions.size.x; let fill = regions.size.x;
let width = piece.width(); let width = piece.width();

View File

@ -2,7 +2,6 @@ use std::cmp::Ordering;
use crate::prelude::*; use crate::prelude::*;
/// # Spacing (H)
/// Insert horizontal spacing into a paragraph. /// Insert horizontal spacing into a paragraph.
/// ///
/// The spacing can be absolute, relative, or fractional. In the last case, the /// The spacing can be absolute, relative, or fractional. In the last case, the
@ -20,64 +19,39 @@ use crate::prelude::*;
/// In [mathematical formulas]($category/math), you can additionally use these /// In [mathematical formulas]($category/math), you can additionally use these
/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`. /// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`.
/// ///
/// ## Parameters /// Display: Spacing (H)
/// - amount: `Spacing` (positional, required) /// Category: layout
/// How much spacing to insert. #[node(Behave)]
///
/// - weak: `bool` (named)
/// If true, the spacing collapses at the start or end of a paragraph.
/// Moreover, from multiple adjacent weak spacings all but the largest one
/// collapse.
///
/// ```example
/// #h(1cm, weak: true)
/// We identified a group of
/// _weak_ specimens that fail to
/// manifest in most cases. However,
/// when #h(8pt, weak: true)
/// supported
/// #h(8pt, weak: true) on both
/// sides, they do show up.
/// ```
///
/// ## Category
/// layout
#[func]
#[capable(Behave)]
#[derive(Debug, Copy, Clone, Hash)]
pub struct HNode { pub struct HNode {
/// The amount of horizontal spacing. /// How much spacing to insert.
#[positional]
#[required]
pub amount: Spacing, pub amount: Spacing,
/// Whether the node is weak, see also [`Behaviour`].
/// If true, the spacing collapses at the start or end of a paragraph.
/// Moreover, from multiple adjacent weak spacings all but the largest one
/// collapse.
///
/// ```example
/// #h(1cm, weak: true)
/// We identified a group of
/// _weak_ specimens that fail to
/// manifest in most cases. However,
/// when #h(8pt, weak: true)
/// supported
/// #h(8pt, weak: true) on both
/// sides, they do show up.
/// ```
#[named]
#[default(false)]
pub weak: bool, pub weak: bool,
} }
#[node]
impl HNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("amount")?;
let weak = args.named("weak")?.unwrap_or(false);
Ok(Self { amount, weak }.pack())
}
}
impl HNode {
/// Normal strong spacing.
pub fn strong(amount: impl Into<Spacing>) -> Self {
Self { amount: amount.into(), weak: false }
}
/// User-created weak spacing.
pub fn weak(amount: impl Into<Spacing>) -> Self {
Self { amount: amount.into(), weak: true }
}
}
impl Behave for HNode { impl Behave for HNode {
fn behaviour(&self) -> Behaviour { fn behaviour(&self) -> Behaviour {
if self.amount.is_fractional() { if self.amount().is_fractional() {
Behaviour::Destructive Behaviour::Destructive
} else if self.weak { } else if self.weak() {
Behaviour::Weak(1) Behaviour::Weak(1)
} else { } else {
Behaviour::Ignorant Behaviour::Ignorant
@ -86,11 +60,10 @@ impl Behave for HNode {
fn larger(&self, prev: &Content) -> bool { fn larger(&self, prev: &Content) -> bool {
let Some(prev) = prev.to::<Self>() else { return false }; let Some(prev) = prev.to::<Self>() else { return false };
self.amount > prev.amount self.amount() > prev.amount()
} }
} }
/// # Spacing (V)
/// Insert vertical spacing into a flow of blocks. /// Insert vertical spacing into a flow of blocks.
/// ///
/// The spacing can be absolute, relative, or fractional. In the last case, /// The spacing can be absolute, relative, or fractional. In the last case,
@ -130,20 +103,24 @@ impl Behave for HNode {
/// #v(4pt, weak: true) /// #v(4pt, weak: true)
/// The proof is simple: /// The proof is simple:
/// ``` /// ```
/// ## Category ///
/// layout /// Display: Spacing (V)
#[func] /// Category: layout
#[capable(Behave)] #[node(Construct, Behave)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
pub struct VNode { pub struct VNode {
/// The amount of vertical spacing. /// The amount of vertical spacing.
#[positional]
#[required]
pub amount: Spacing, pub amount: Spacing,
/// The node's weakness level, see also [`Behaviour`]. /// The node's weakness level, see also [`Behaviour`].
pub weakness: u8, #[named]
#[skip]
#[default]
pub weakness: usize,
} }
#[node] impl Construct for VNode {
impl VNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("spacing")?; let amount = args.expect("spacing")?;
let node = if args.named("weak")?.unwrap_or(false) { let node = if args.named("weak")?.unwrap_or(false) {
@ -158,36 +135,36 @@ impl VNode {
impl VNode { impl VNode {
/// Normal strong spacing. /// Normal strong spacing.
pub fn strong(amount: Spacing) -> Self { pub fn strong(amount: Spacing) -> Self {
Self { amount, weakness: 0 } Self::new(amount).with_weakness(0)
} }
/// User-created weak spacing. /// User-created weak spacing.
pub fn weak(amount: Spacing) -> Self { pub fn weak(amount: Spacing) -> Self {
Self { amount, weakness: 1 } Self::new(amount).with_weakness(1)
} }
/// Weak spacing with list attach weakness. /// Weak spacing with list attach weakness.
pub fn list_attach(amount: Spacing) -> Self { pub fn list_attach(amount: Spacing) -> Self {
Self { amount, weakness: 2 } Self::new(amount).with_weakness(2)
} }
/// Weak spacing with BlockNode::ABOVE/BELOW weakness. /// Weak spacing with BlockNode::ABOVE/BELOW weakness.
pub fn block_around(amount: Spacing) -> Self { pub fn block_around(amount: Spacing) -> Self {
Self { amount, weakness: 3 } Self::new(amount).with_weakness(3)
} }
/// Weak spacing with BlockNode::SPACING weakness. /// Weak spacing with BlockNode::SPACING weakness.
pub fn block_spacing(amount: Spacing) -> Self { pub fn block_spacing(amount: Spacing) -> Self {
Self { amount, weakness: 4 } Self::new(amount).with_weakness(4)
} }
} }
impl Behave for VNode { impl Behave for VNode {
fn behaviour(&self) -> Behaviour { fn behaviour(&self) -> Behaviour {
if self.amount.is_fractional() { if self.amount().is_fractional() {
Behaviour::Destructive Behaviour::Destructive
} else if self.weakness > 0 { } else if self.weakness() > 0 {
Behaviour::Weak(self.weakness) Behaviour::Weak(self.weakness())
} else { } else {
Behaviour::Ignorant Behaviour::Ignorant
} }
@ -195,10 +172,15 @@ impl Behave for VNode {
fn larger(&self, prev: &Content) -> bool { fn larger(&self, prev: &Content) -> bool {
let Some(prev) = prev.to::<Self>() else { return false }; let Some(prev) = prev.to::<Self>() else { return false };
self.amount > prev.amount self.amount() > prev.amount()
} }
} }
cast_from_value! {
VNode,
v: Content => v.to::<Self>().cloned().ok_or("expected vnode")?,
}
/// Kinds of spacing. /// Kinds of spacing.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Spacing { pub enum Spacing {
@ -214,22 +196,6 @@ impl Spacing {
pub fn is_fractional(self) -> bool { pub fn is_fractional(self) -> bool {
matches!(self, Self::Fr(_)) matches!(self, Self::Fr(_))
} }
/// Encode into a value.
pub fn encode(self) -> Value {
match self {
Self::Rel(rel) => {
if rel.rel.is_zero() {
Value::Length(rel.abs)
} else if rel.abs.is_zero() {
Value::Ratio(rel.rel)
} else {
Value::Relative(rel)
}
}
Self::Fr(fr) => Value::Fraction(fr),
}
}
} }
impl From<Abs> for Spacing { impl From<Abs> for Spacing {
@ -244,6 +210,12 @@ impl From<Em> for Spacing {
} }
} }
impl From<Fr> for Spacing {
fn from(fr: Fr) -> Self {
Self::Fr(fr)
}
}
impl PartialOrd for Spacing { impl PartialOrd for Spacing {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) { match (self, other) {
@ -254,8 +226,23 @@ impl PartialOrd for Spacing {
} }
} }
castable! { cast_from_value! {
Spacing, Spacing,
v: Rel<Length> => Self::Rel(v), v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(v), v: Fr => Self::Fr(v),
} }
cast_to_value! {
v: Spacing => match v {
Spacing::Rel(rel) => {
if rel.rel.is_zero() {
Value::Length(rel.abs)
} else if rel.abs.is_zero() {
Value::Ratio(rel.rel)
} else {
Value::Relative(rel)
}
}
Spacing::Fr(fr) => Value::Fraction(fr),
}
}

View File

@ -3,7 +3,6 @@ use typst::model::StyledNode;
use super::{AlignNode, Spacing}; use super::{AlignNode, Spacing};
use crate::prelude::*; use crate::prelude::*;
/// # Stack
/// Arrange content and spacing horizontally or vertically. /// Arrange content and spacing horizontally or vertically.
/// ///
/// The stack places a list of items along an axis, with optional spacing /// The stack places a list of items along an axis, with optional spacing
@ -19,45 +18,28 @@ use crate::prelude::*;
/// ) /// )
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Stack
/// - items: `StackChild` (positional, variadic) /// Category: layout
/// The items to stack along an axis. #[node(Layout)]
///
/// - dir: `Dir` (named)
/// The direction along which the items are stacked. Possible values are:
///
/// - `{ltr}`: Left to right.
/// - `{rtl}`: Right to left.
/// - `{ttb}`: Top to bottom.
/// - `{btt}`: Bottom to top.
///
/// - spacing: `Spacing` (named)
/// Spacing to insert between items where no explicit spacing was provided.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct StackNode { pub struct StackNode {
/// The stacking direction. /// The childfren to stack along the axis.
pub dir: Dir, #[variadic]
/// The spacing between non-spacing children.
pub spacing: Option<Spacing>,
/// The children to be stacked.
pub children: Vec<StackChild>, pub children: Vec<StackChild>,
}
#[node] /// The direction along which the items are stacked. Possible values are:
impl StackNode { ///
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { /// - `{ltr}`: Left to right.
Ok(Self { /// - `{rtl}`: Right to left.
dir: args.named("dir")?.unwrap_or(Dir::TTB), /// - `{ttb}`: Top to bottom.
spacing: args.named("spacing")?, /// - `{btt}`: Bottom to top.
children: args.all()?, #[named]
} #[default(Dir::TTB)]
.pack()) pub dir: Dir,
}
/// Spacing to insert between items where no explicit spacing was provided.
#[named]
#[default]
pub spacing: Option<Spacing>,
} }
impl Layout for StackNode { impl Layout for StackNode {
@ -67,15 +49,16 @@ impl Layout for StackNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut layouter = StackLayouter::new(self.dir, regions, styles); let mut layouter = StackLayouter::new(self.dir(), regions, styles);
// Spacing to insert before the next block. // Spacing to insert before the next block.
let spacing = self.spacing();
let mut deferred = None; let mut deferred = None;
for child in &self.children { for child in self.children() {
match child { match child {
StackChild::Spacing(kind) => { StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind); layouter.layout_spacing(kind);
deferred = None; deferred = None;
} }
StackChild::Block(block) => { StackChild::Block(block) => {
@ -83,8 +66,8 @@ impl Layout for StackNode {
layouter.layout_spacing(kind); layouter.layout_spacing(kind);
} }
layouter.layout_block(vt, block, styles)?; layouter.layout_block(vt, &block, styles)?;
deferred = self.spacing; deferred = spacing;
} }
} }
} }
@ -111,10 +94,17 @@ impl Debug for StackChild {
} }
} }
castable! { cast_from_value! {
StackChild, StackChild,
spacing: Spacing => Self::Spacing(spacing), v: Spacing => Self::Spacing(v),
content: Content => Self::Block(content), v: Content => Self::Block(v),
}
cast_to_value! {
v: StackChild => match v {
StackChild::Spacing(spacing) => spacing.into(),
StackChild::Block(content) => content.into(),
}
} }
/// Performs stack layout. /// Performs stack layout.
@ -212,9 +202,9 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignNode` is respected // Block-axis alignment of the `AlignNode` is respected
// by the stack node. // by the stack node.
let aligns = if let Some(styled) = block.to::<StyledNode>() { let aligns = if let Some(styled) = block.to::<StyledNode>() {
styles.chain(&styled.map).get(AlignNode::ALIGNS) styles.chain(&styled.map()).get(AlignNode::ALIGNMENT)
} else { } else {
styles.get(AlignNode::ALIGNS) styles.get(AlignNode::ALIGNMENT)
}; };
let aligns = aligns.resolve(styles); let aligns = aligns.resolve(styles);

View File

@ -1,7 +1,6 @@
use crate::layout::{AlignNode, GridLayouter, Sizing, TrackSizings}; use crate::layout::{AlignNode, GridLayouter, TrackSizings};
use crate::prelude::*; use crate::prelude::*;
/// # Table
/// A table of items. /// A table of items.
/// ///
/// Tables are used to arrange content in cells. Cells can contain arbitrary /// Tables are used to arrange content in cells. Cells can contain arbitrary
@ -31,47 +30,46 @@ use crate::prelude::*;
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
/// - cells: `Content` (positional, variadic)
/// The contents of the table cells.
///
/// - rows: `TrackSizings` (named)
/// Defines the row sizes.
/// See the [grid documentation]($func/grid) for more information on track
/// sizing.
///
/// - columns: `TrackSizings` (named)
/// Defines the column sizes.
/// See the [grid documentation]($func/grid) for more information on track
/// sizing.
///
/// - gutter: `TrackSizings` (named) /// - gutter: `TrackSizings` (named)
/// Defines the gaps between rows & columns. /// Defines the gaps between rows & columns.
/// See the [grid documentation]($func/grid) for more information on gutters. /// See the [grid documentation]($func/grid) for more information on gutters.
/// ///
/// - column-gutter: `TrackSizings` (named) /// Display: Table
/// Defines the gaps between columns. Takes precedence over `gutter`. /// Category: layout
/// See the [grid documentation]($func/grid) for more information on gutters. #[node(Layout)]
///
/// - row-gutter: `TrackSizings` (named)
/// Defines the gaps between rows. Takes precedence over `gutter`.
/// See the [grid documentation]($func/grid) for more information on gutters.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct TableNode { pub struct TableNode {
/// Defines sizing for content rows and columns. /// The contents of the table cells.
pub tracks: Axes<Vec<Sizing>>, #[variadic]
/// Defines sizing of gutter rows and columns between content.
pub gutter: Axes<Vec<Sizing>>,
/// The content to be arranged in the table.
pub cells: Vec<Content>, pub cells: Vec<Content>,
}
#[node] /// Defines the column sizes.
impl TableNode { /// See the [grid documentation]($func/grid) for more information on track
/// sizing.
#[named]
#[default]
pub columns: TrackSizings,
/// Defines the row sizes.
/// See the [grid documentation]($func/grid) for more information on track
/// sizing.
#[named]
#[default]
pub rows: TrackSizings,
/// Defines the gaps between columns. Takes precedence over `gutter`.
/// See the [grid documentation]($func/grid) for more information on gutters.
#[named]
#[shorthand(gutter)]
#[default]
pub column_gutter: TrackSizings,
/// Defines the gaps between rows. Takes precedence over `gutter`.
/// See the [grid documentation]($func/grid) for more information on gutters.
#[named]
#[shorthand(gutter)]
#[default]
pub row_gutter: TrackSizings,
/// How to fill the cells. /// How to fill the cells.
/// ///
/// This can be a color or a function that returns a color. The function is /// This can be a color or a function that returns a color. The function is
@ -92,58 +90,35 @@ impl TableNode {
/// [Profit:], [500 €], [1000 €], [1500 €], /// [Profit:], [500 €], [1000 €], [1500 €],
/// ) /// )
/// ``` /// ```
#[property(referenced)] #[settable]
pub const FILL: Celled<Option<Paint>> = Celled::Value(None); #[default]
pub fill: Celled<Option<Paint>>,
/// How to align the cell's content. /// How to align the cell's content.
/// ///
/// This can either be a single alignment or a function that returns an /// This can either be a single alignment or a function that returns an
/// alignment. The function is passed the cell's column and row index, /// alignment. The function is passed the cell's column and row index,
/// starting at zero. If set to `{auto}`, the outer alignment is used. /// starting at zero. If set to `{auto}`, the outer alignment is used.
#[property(referenced)] #[settable]
pub const ALIGN: Celled<Smart<Axes<Option<GenAlign>>>> = Celled::Value(Smart::Auto); #[default]
pub align: Celled<Smart<Axes<Option<GenAlign>>>>,
/// How to stroke the cells. /// How to stroke the cells.
/// ///
/// This can be a color, a stroke width, both, or `{none}` to disable /// This can be a color, a stroke width, both, or `{none}` to disable
/// the stroke. /// the stroke.
#[property(resolve, fold)] #[settable]
pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default()); #[resolve]
#[fold]
#[default(Some(PartialStroke::default()))]
pub stroke: Option<PartialStroke>,
/// How much to pad the cells's content. /// How much to pad the cells's content.
/// ///
/// The default value is `{5pt}`. /// The default value is `{5pt}`.
pub const INSET: Rel<Length> = Abs::pt(5.0).into(); #[settable]
#[default(Abs::pt(5.0).into())]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub inset: Rel<Length>,
let TrackSizings(columns) = args.named("columns")?.unwrap_or_default();
let TrackSizings(rows) = args.named("rows")?.unwrap_or_default();
let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default();
let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v);
let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v);
Ok(Self {
tracks: Axes::new(columns, rows),
gutter: Axes::new(
column_gutter.unwrap_or_else(|| base_gutter.clone()),
row_gutter.unwrap_or(base_gutter),
),
cells: args.all()?,
}
.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"columns" => Some(Sizing::encode_slice(&self.tracks.x)),
"rows" => Some(Sizing::encode_slice(&self.tracks.y)),
"column-gutter" => Some(Sizing::encode_slice(&self.gutter.x)),
"row-gutter" => Some(Sizing::encode_slice(&self.gutter.y)),
"cells" => Some(Value::Array(
self.cells.iter().cloned().map(Value::Content).collect(),
)),
_ => None,
}
}
} }
impl Layout for TableNode { impl Layout for TableNode {
@ -156,11 +131,12 @@ impl Layout for TableNode {
let inset = styles.get(Self::INSET); let inset = styles.get(Self::INSET);
let align = styles.get(Self::ALIGN); let align = styles.get(Self::ALIGN);
let cols = self.tracks.x.len().max(1); let tracks = Axes::new(self.columns().0, self.rows().0);
let gutter = Axes::new(self.column_gutter().0, self.row_gutter().0);
let cols = tracks.x.len().max(1);
let cells: Vec<_> = self let cells: Vec<_> = self
.cells .cells()
.iter() .into_iter()
.cloned()
.enumerate() .enumerate()
.map(|(i, child)| { .map(|(i, child)| {
let mut child = child.padded(Sides::splat(inset)); let mut child = child.padded(Sides::splat(inset));
@ -168,7 +144,7 @@ impl Layout for TableNode {
let x = i % cols; let x = i % cols;
let y = i / cols; let y = i / cols;
if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
child = child.styled(AlignNode::ALIGNS, alignment) child = child.styled(AlignNode::ALIGNMENT, alignment)
} }
Ok(child) Ok(child)
@ -181,8 +157,8 @@ impl Layout for TableNode {
// Prepare grid layout by unifying content and gutter tracks. // Prepare grid layout by unifying content and gutter tracks.
let layouter = GridLayouter::new( let layouter = GridLayouter::new(
vt, vt,
self.tracks.as_deref(), tracks.as_deref(),
self.gutter.as_deref(), gutter.as_deref(),
&cells, &cells,
regions, regions,
styles, styles,
@ -269,6 +245,12 @@ impl<T: Cast + Clone> Celled<T> {
} }
} }
impl<T: Default> Default for Celled<T> {
fn default() -> Self {
Self::Value(T::default())
}
}
impl<T: Cast> Cast for Celled<T> { impl<T: Cast> Cast for Celled<T> {
fn is(value: &Value) -> bool { fn is(value: &Value) -> bool {
matches!(value, Value::Func(_)) || T::is(value) matches!(value, Value::Func(_)) || T::is(value)
@ -286,3 +268,12 @@ impl<T: Cast> Cast for Celled<T> {
T::describe() + CastInfo::Type("function") T::describe() + CastInfo::Type("function")
} }
} }
impl<T: Into<Value>> From<Celled<T>> for Value {
fn from(celled: Celled<T>) -> Self {
match celled {
Celled::Value(value) => value.into(),
Celled::Func(func) => func.into(),
}
}
}

View File

@ -1,8 +1,7 @@
use crate::layout::{BlockNode, GridNode, HNode, ParNode, Sizing, Spacing}; use crate::layout::{BlockNode, GridLayouter, HNode, ParNode, Sizing, Spacing};
use crate::prelude::*; use crate::prelude::*;
use crate::text::{SpaceNode, TextNode}; use crate::text::{SpaceNode, TextNode};
/// # Term List
/// A list of terms and their descriptions. /// A list of terms and their descriptions.
/// ///
/// Displays a sequence of terms and their descriptions vertically. When the /// Displays a sequence of terms and their descriptions vertically. When the
@ -20,55 +19,49 @@ use crate::text::{SpaceNode, TextNode};
/// between two adjacent letters. /// between two adjacent letters.
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Term List
/// - items: `Content` (positional, variadic) /// Category: layout
/// The term list's children. #[node(Layout)]
///
/// When using the term list syntax, adjacent items are automatically
/// collected into term lists, even through constructs like for loops.
///
/// ```example
/// #for year, product in (
/// "1978": "TeX",
/// "1984": "LaTeX",
/// "2019": "Typst",
/// ) [/ #product: Born in #year.]
/// ```
///
/// - tight: `bool` (named)
/// 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.
///
/// ```example
/// / Fact: If a term list has a lot
/// of text, and maybe other inline
/// content, it should not be tight
/// anymore.
///
/// / Tip: To make it wide, simply
/// insert a blank line between the
/// items.
/// ```
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct TermsNode { pub struct TermsNode {
/// If true, the items are separated by leading instead of list spacing. /// The term list's children.
pub tight: bool, ///
/// The individual bulleted or numbered items. /// When using the term list syntax, adjacent items are automatically
pub items: StyleVec<TermItem>, /// collected into term lists, even through constructs like for loops.
} ///
/// ```example
/// #for year, product in (
/// "1978": "TeX",
/// "1984": "LaTeX",
/// "2019": "Typst",
/// ) [/ #product: Born in #year.]
/// ```
#[variadic]
pub items: Vec<TermItem>,
/// 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.
///
/// ```example
/// / Fact: If a term list has a lot
/// of text, and maybe other inline
/// content, it should not be tight
/// anymore.
///
/// / Tip: To make it wide, simply
/// insert a blank line between the
/// items.
/// ```
#[named]
#[default(true)]
pub tight: bool,
#[node]
impl TermsNode {
/// The indentation of each item's term. /// The indentation of each item's term.
#[property(resolve)] #[settable]
pub const INDENT: Length = Length::zero(); #[resolve]
#[default]
pub indent: Length,
/// The hanging indent of the description. /// The hanging indent of the description.
/// ///
@ -77,31 +70,17 @@ impl TermsNode {
/// / Term: This term list does not /// / Term: This term list does not
/// make use of hanging indents. /// make use of hanging indents.
/// ``` /// ```
#[property(resolve)] #[settable]
pub const HANGING_INDENT: Length = Em::new(1.0).into(); #[resolve]
#[default(Em::new(1.0).into())]
pub hanging_indent: Length,
/// The spacing between the items of a wide (non-tight) term list. /// 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]($func/block.below).
pub const SPACING: Smart<Spacing> = Smart::Auto; #[settable]
#[default]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub spacing: Smart<Spacing>,
Ok(Self {
tight: args.named("tight")?.unwrap_or(true),
items: args.all()?.into_iter().collect(),
}
.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"tight" => Some(Value::Bool(self.tight)),
"items" => {
Some(Value::Array(self.items.items().map(|item| item.encode()).collect()))
}
_ => None,
}
}
} }
impl Layout for TermsNode { impl Layout for TermsNode {
@ -113,66 +92,63 @@ impl Layout for TermsNode {
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = styles.get(Self::INDENT); let indent = styles.get(Self::INDENT);
let body_indent = styles.get(Self::HANGING_INDENT); let body_indent = styles.get(Self::HANGING_INDENT);
let gutter = if self.tight { let gutter = if self.tight() {
styles.get(ParNode::LEADING).into() styles.get(ParNode::LEADING).into()
} else { } else {
styles styles
.get(Self::SPACING) .get(Self::SPACING)
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount())
}; };
let mut cells = vec![]; let mut cells = vec![];
for (item, map) in self.items.iter() { for item in self.items() {
let body = Content::sequence(vec![ let body = Content::sequence(vec![
HNode { amount: (-body_indent).into(), weak: false }.pack(), HNode::new((-body_indent).into()).pack(),
(item.term.clone() + TextNode::packed(':')).strong(), (item.term() + TextNode::packed(':')).strong(),
SpaceNode.pack(), SpaceNode::new().pack(),
item.description.clone(), item.description(),
]); ]);
cells.push(Content::empty()); cells.push(Content::empty());
cells.push(body.styled_with_map(map.clone())); cells.push(body);
} }
GridNode { let layouter = GridLayouter::new(
tracks: Axes::with_x(vec![ vt,
Sizing::Rel((indent + body_indent).into()), Axes::with_x(&[Sizing::Rel((indent + body_indent).into()), Sizing::Auto]),
Sizing::Auto, Axes::with_y(&[gutter.into()]),
]), &cells,
gutter: Axes::with_y(vec![gutter.into()]), regions,
cells, styles,
} );
.layout(vt, styles, regions)
Ok(layouter.layout()?.fragment)
} }
} }
/// A term list item. /// A term list item.
#[derive(Debug, Clone, Hash)] #[node]
pub struct TermItem { pub struct TermItem {
/// The term described by the list item. /// The term described by the list item.
#[positional]
#[required]
pub term: Content, pub term: Content,
/// The description of the term. /// The description of the term.
#[positional]
#[required]
pub description: Content, pub description: Content,
} }
impl TermItem { cast_from_value! {
/// Encode the item into a value.
fn encode(&self) -> Value {
Value::Array(array![
Value::Content(self.term.clone()),
Value::Content(self.description.clone()),
])
}
}
castable! {
TermItem, TermItem,
array: Array => { array: Array => {
let mut iter = array.into_iter(); let mut iter = array.into_iter();
let (term, description) = match (iter.next(), iter.next(), iter.next()) { let (term, description) = match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => (a.cast()?, b.cast()?), (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
_ => Err("term array must contain exactly two entries")?, _ => Err("array must contain exactly two entries")?,
}; };
Self { term, description } Self::new(term, description)
}, },
v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?,
} }

View File

@ -2,7 +2,6 @@ use typst::geom::Transform;
use crate::prelude::*; use crate::prelude::*;
/// # Move
/// Move content without affecting layout. /// Move content without affecting layout.
/// ///
/// The `move` function allows you to move content while the layout still 'sees' /// The `move` function allows you to move content while the layout still 'sees'
@ -22,39 +21,24 @@ use crate::prelude::*;
/// )) /// ))
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Move
/// - body: `Content` (positional, required) /// Category: layout
/// The content to move. #[node(Layout)]
///
/// - dx: `Rel<Length>` (named)
/// The horizontal displacement of the content.
///
/// - dy: `Rel<Length>` (named)
/// The vertical displacement of the content.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct MoveNode { pub struct MoveNode {
/// The offset by which to move the content. /// The content to move.
pub delta: Axes<Rel<Length>>, #[positional]
/// The content that should be moved. #[required]
pub body: Content, pub body: Content,
}
#[node] /// The horizontal displacement of the content.
impl MoveNode { #[named]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let dx = args.named("dx")?.unwrap_or_default(); pub dx: Rel<Length>,
let dy = args.named("dy")?.unwrap_or_default();
Ok(Self { /// The vertical displacement of the content.
delta: Axes::new(dx, dy), #[named]
body: args.expect("body")?, #[default]
} pub dy: Rel<Length>,
.pack())
}
} }
impl Layout for MoveNode { impl Layout for MoveNode {
@ -65,15 +49,14 @@ impl Layout for MoveNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pod = Regions::one(regions.base(), Axes::splat(false)); let pod = Regions::one(regions.base(), Axes::splat(false));
let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
let delta = self.delta.resolve(styles); let delta = Axes::new(self.dx(), self.dy()).resolve(styles);
let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s));
frame.translate(delta.to_point()); frame.translate(delta.to_point());
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }
} }
/// # Rotate
/// Rotate content with affecting layout. /// Rotate content with affecting layout.
/// ///
/// Rotate an element by a given angle. The layout will act as if the element /// Rotate an element by a given angle. The layout will act as if the element
@ -89,31 +72,26 @@ impl Layout for MoveNode {
/// ) /// )
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Rotate
/// - body: `Content` (positional, required) /// Category: layout
/// The content to rotate. #[node(Layout)]
///
/// - angle: `Angle` (named)
/// The amount of rotation.
///
/// ```example
/// #rotate(angle: -1.571rad)[Space!]
/// ```
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct RotateNode { pub struct RotateNode {
/// The angle by which to rotate the node. /// The amount of rotation.
///
/// ```example
/// #rotate(angle: -1.571rad)[Space!]
/// ```
///
#[named]
#[shorthand]
#[default]
pub angle: Angle, pub angle: Angle,
/// The content that should be rotated.
pub body: Content,
}
#[node] /// The content to rotate.
impl RotateNode { #[positional]
#[required]
pub body: Content,
/// The origin of the rotation. /// The origin of the rotation.
/// ///
/// By default, the origin is the center of the rotated element. If, /// By default, the origin is the center of the rotated element. If,
@ -130,16 +108,10 @@ impl RotateNode {
/// #box(rotate(angle: 30deg, origin: top + left, square())) /// #box(rotate(angle: 30deg, origin: top + left, square()))
/// #box(rotate(angle: 30deg, origin: bottom + right, square())) /// #box(rotate(angle: 30deg, origin: bottom + right, square()))
/// ``` /// ```
#[property(resolve)] #[settable]
pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default(); #[resolve]
#[default]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub origin: Axes<Option<GenAlign>>,
Ok(Self {
angle: args.named_or_find("angle")?.unwrap_or_default(),
body: args.expect("body")?,
}
.pack())
}
} }
impl Layout for RotateNode { impl Layout for RotateNode {
@ -150,18 +122,17 @@ impl Layout for RotateNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pod = Regions::one(regions.base(), Axes::splat(false)); let pod = Regions::one(regions.base(), Axes::splat(false));
let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s));
let ts = Transform::translate(x, y) let ts = Transform::translate(x, y)
.pre_concat(Transform::rotate(self.angle)) .pre_concat(Transform::rotate(self.angle()))
.pre_concat(Transform::translate(-x, -y)); .pre_concat(Transform::translate(-x, -y));
frame.transform(ts); frame.transform(ts);
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }
} }
/// # Scale
/// Scale content without affecting layout. /// Scale content without affecting layout.
/// ///
/// The `scale` function allows you to scale and mirror content without /// The `scale` function allows you to scale and mirror content without
@ -174,34 +145,29 @@ impl Layout for RotateNode {
/// #scale(x: -100%)[This is mirrored.] /// #scale(x: -100%)[This is mirrored.]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Scale
/// - body: `Content` (positional, required) /// Category: layout
/// The content to scale. #[node(Construct, Layout)]
///
/// - x: `Ratio` (named)
/// The horizontal scaling factor.
///
/// The body will be mirrored horizontally if the parameter is negative.
///
/// - y: `Ratio` (named)
/// The vertical scaling factor.
///
/// The body will be mirrored vertically if the parameter is negative.
///
/// ## Category
/// layout
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct ScaleNode { pub struct ScaleNode {
/// Scaling factor. /// The content to scale.
pub factor: Axes<Ratio>, #[positional]
/// The content that should be scaled. #[required]
pub body: Content, pub body: Content,
}
#[node] /// The horizontal scaling factor.
impl ScaleNode { ///
/// The body will be mirrored horizontally if the parameter is negative.
#[named]
#[default(Ratio::one())]
pub x: Ratio,
/// The vertical scaling factor.
///
/// The body will be mirrored vertically if the parameter is negative.
#[named]
#[default(Ratio::one())]
pub y: Ratio,
/// The origin of the transformation. /// The origin of the transformation.
/// ///
/// By default, the origin is the center of the scaled element. /// By default, the origin is the center of the scaled element.
@ -210,18 +176,18 @@ impl ScaleNode {
/// A#box(scale(75%)[A])A \ /// A#box(scale(75%)[A])A \
/// B#box(scale(75%, origin: bottom + left)[B])B /// B#box(scale(75%, origin: bottom + left)[B])B
/// ``` /// ```
#[property(resolve)] #[settable]
pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default(); #[resolve]
#[default]
pub origin: Axes<Option<GenAlign>>,
}
impl Construct for ScaleNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let all = args.find()?; let all = args.find()?;
let x = args.named("x")?.or(all).unwrap_or(Ratio::one()); let x = args.named("x")?.or(all).unwrap_or(Ratio::one());
let y = args.named("y")?.or(all).unwrap_or(Ratio::one()); let y = args.named("y")?.or(all).unwrap_or(Ratio::one());
Ok(Self { Ok(Self::new(args.expect::<Content>("body")?).with_x(x).with_y(y).pack())
factor: Axes::new(x, y),
body: args.expect("body")?,
}
.pack())
} }
} }
@ -233,11 +199,11 @@ impl Layout for ScaleNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pod = Regions::one(regions.base(), Axes::splat(false)); let pod = Regions::one(regions.base(), Axes::splat(false));
let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s));
let transform = Transform::translate(x, y) let transform = Transform::translate(x, y)
.pre_concat(Transform::scale(self.factor.x, self.factor.y)) .pre_concat(Transform::scale(self.x(), self.y()))
.pre_concat(Transform::translate(-x, -y)); .pre_concat(Transform::translate(-x, -y));
frame.transform(transform); frame.transform(transform);
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))

View File

@ -19,7 +19,7 @@ use self::layout::LayoutRoot;
/// Construct the standard library. /// Construct the standard library.
pub fn build() -> Library { pub fn build() -> Library {
let math = math::module(); let math = math::module();
let calc = compute::calc(); let calc = compute::calc::module();
let global = global(math.clone(), calc); let global = global(math.clone(), calc);
Library { global, math, styles: styles(), items: items() } Library { global, math, styles: styles(), items: items() }
} }
@ -166,37 +166,37 @@ fn items() -> LangItems {
layout: |world, content, styles| content.layout_root(world, styles), layout: |world, content, styles| content.layout_root(world, styles),
em: |styles| styles.get(text::TextNode::SIZE), em: |styles| styles.get(text::TextNode::SIZE),
dir: |styles| styles.get(text::TextNode::DIR), dir: |styles| styles.get(text::TextNode::DIR),
space: || text::SpaceNode.pack(), space: || text::SpaceNode::new().pack(),
linebreak: || text::LinebreakNode { justify: false }.pack(), linebreak: || text::LinebreakNode::new().pack(),
text: |text| text::TextNode(text).pack(), text: |text| text::TextNode::new(text).pack(),
text_id: NodeId::of::<text::TextNode>(), text_id: NodeId::of::<text::TextNode>(),
text_str: |content| Some(&content.to::<text::TextNode>()?.0), text_str: |content| Some(content.to::<text::TextNode>()?.text()),
smart_quote: |double| text::SmartQuoteNode { double }.pack(), smart_quote: |double| text::SmartQuoteNode::new().with_double(double).pack(),
parbreak: || layout::ParbreakNode.pack(), parbreak: || layout::ParbreakNode::new().pack(),
strong: |body| text::StrongNode(body).pack(), strong: |body| text::StrongNode::new(body).pack(),
emph: |body| text::EmphNode(body).pack(), emph: |body| text::EmphNode::new(body).pack(),
raw: |text, lang, block| { raw: |text, lang, block| {
let content = text::RawNode { text, block }.pack(); let content = text::RawNode::new(text).with_block(block).pack();
match lang { match lang {
Some(_) => content.styled(text::RawNode::LANG, lang), Some(_) => content.styled(text::RawNode::LANG, lang),
None => content, None => content,
} }
}, },
link: |url| meta::LinkNode::from_url(url).pack(), link: |url| meta::LinkNode::from_url(url).pack(),
ref_: |target| meta::RefNode(target).pack(), ref_: |target| meta::RefNode::new(target).pack(),
heading: |level, body| meta::HeadingNode { level, title: body }.pack(), heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(),
list_item: |body| layout::ListItem::List(body).pack(), list_item: |body| layout::ListItem::new(body).pack(),
enum_item: |number, body| layout::ListItem::Enum(number, body).pack(), enum_item: |number, body| layout::EnumItem::new(body).with_number(number).pack(),
term_item: |term, description| { term_item: |term, description| layout::TermItem::new(term, description).pack(),
layout::ListItem::Term(layout::TermItem { term, description }).pack() formula: |body, block| math::FormulaNode::new(body).with_block(block).pack(),
math_align_point: || math::AlignPointNode::new().pack(),
math_delimited: |open, body, close| math::LrNode::new(open + body + close).pack(),
math_attach: |base, bottom, top| {
math::AttachNode::new(base).with_bottom(bottom).with_top(top).pack()
}, },
formula: |body, block| math::FormulaNode { body, block }.pack(), math_accent: |base, accent| {
math_align_point: || math::AlignPointNode.pack(), math::AccentNode::new(base, math::Accent::new(accent)).pack()
math_delimited: |open, body, close| {
math::LrNode { body: open + body + close, size: None }.pack()
}, },
math_attach: |base, bottom, top| math::AttachNode { base, bottom, top }.pack(), math_frac: |num, denom| math::FracNode::new(num, denom).pack(),
math_accent: |base, accent| math::AccentNode { base, accent }.pack(),
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
} }
} }

View File

@ -5,7 +5,6 @@ use super::*;
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5); const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// # Accent
/// Attach an accent to a base. /// Attach an accent to a base.
/// ///
/// ## Example /// ## Example
@ -15,72 +14,48 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// $tilde(a) = accent(a, \u{0303})$ /// $tilde(a) = accent(a, \u{0303})$
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Accent
/// - base: `Content` (positional, required) /// Category: math
/// The base to which the accent is applied. #[node(LayoutMath)]
/// May consist of multiple letters.
///
/// ```example
/// $arrow(A B C)$
/// ```
///
/// - accent: `char` (positional, required)
/// The accent to apply to the base.
///
/// Supported accents include:
///
/// | Accent | Name | Codepoint |
/// | ------------ | --------------- | --------- |
/// | Grave | `grave` | <code>&DiacriticalGrave;</code> |
/// | Acute | `acute` | `´` |
/// | Circumflex | `hat` | `^` |
/// | Tilde | `tilde` | `~` |
/// | Macron | `macron` | `¯` |
/// | Breve | `breve` | `˘` |
/// | Dot | `dot` | `.` |
/// | Diaeresis | `diaer` | `¨` |
/// | Circle | `circle` | `∘` |
/// | Double acute | `acute.double` | `˝` |
/// | Caron | `caron` | `ˇ` |
/// | Right arrow | `arrow`, `->` | `→` |
/// | Left arrow | `arrow.l`, `<-` | `←` |
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct AccentNode { pub struct AccentNode {
/// The accent base. /// The base to which the accent is applied.
/// May consist of multiple letters.
///
/// ```example
/// $arrow(A B C)$
/// ```
#[positional]
#[required]
pub base: Content, pub base: Content,
/// The accent.
pub accent: char,
}
#[node] /// The accent to apply to the base.
impl AccentNode { ///
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { /// Supported accents include:
let base = args.expect("base")?; ///
let accent = args.expect::<Accent>("accent")?.0; /// | Accent | Name | Codepoint |
Ok(Self { base, accent }.pack()) /// | ------------ | --------------- | --------- |
} /// | Grave | `grave` | <code>&DiacriticalGrave;</code> |
} /// | Acute | `acute` | `´` |
/// | Circumflex | `hat` | `^` |
struct Accent(char); /// | Tilde | `tilde` | `~` |
/// | Macron | `macron` | `¯` |
castable! { /// | Breve | `breve` | `˘` |
Accent, /// | Dot | `dot` | `.` |
v: char => Self(v), /// | Diaeresis | `diaer` | `¨` |
v: Content => match v.to::<TextNode>() { /// | Circle | `circle` | `∘` |
Some(text) => Self(Value::Str(text.0.clone().into()).cast()?), /// | Double acute | `acute.double` | `˝` |
None => Err("expected text")?, /// | Caron | `caron` | `ˇ` |
}, /// | Right arrow | `arrow`, `->` | `→` |
/// | Left arrow | `arrow.l`, `<-` | `←` |
#[positional]
#[required]
pub accent: Accent,
} }
impl LayoutMath for AccentNode { impl LayoutMath for AccentNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_cramped(true)); ctx.style(ctx.style.with_cramped(true));
let base = ctx.layout_fragment(&self.base)?; let base = ctx.layout_fragment(&self.base())?;
ctx.unstyle(); ctx.unstyle();
let base_attach = match &base { let base_attach = match &base {
@ -92,7 +67,7 @@ impl LayoutMath for AccentNode {
// Forcing the accent to be at least as large as the base makes it too // Forcing the accent to be at least as large as the base makes it too
// wide in many case. // wide in many case.
let c = combining_accent(self.accent).unwrap_or(self.accent); let Accent(c) = self.accent();
let glyph = GlyphFragment::new(ctx, c); let glyph = GlyphFragment::new(ctx, c);
let short_fall = ACCENT_SHORT_FALL.scaled(ctx); let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall);
@ -136,3 +111,26 @@ fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs {
(advance.scaled(ctx) + italics_correction) / 2.0 (advance.scaled(ctx) + italics_correction) / 2.0
}) })
} }
/// An accent character.
pub struct Accent(char);
impl Accent {
/// Normalize a character into an accent.
pub fn new(c: char) -> Self {
Self(combining_accent(c).unwrap_or(c))
}
}
cast_from_value! {
Accent,
v: char => Self::new(v),
v: Content => match v.to::<TextNode>() {
Some(node) => Value::Str(node.text().into()).cast()?,
None => Err("expected text")?,
},
}
cast_to_value! {
v: Accent => v.0.into()
}

View File

@ -1,21 +1,11 @@
use super::*; use super::*;
/// # Alignment Point
/// A math alignment point: `&`, `&&`. /// A math alignment point: `&`, `&&`.
/// ///
/// ## Parameters /// Display: Alignment Point
/// - index: `usize` (positional, required) /// Category: math
/// The alignment point's index. #[node(LayoutMath)]
/// pub struct AlignPointNode {}
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct AlignPointNode;
#[node]
impl AlignPointNode {}
impl LayoutMath for AlignPointNode { impl LayoutMath for AlignPointNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {

View File

@ -1,6 +1,5 @@
use super::*; use super::*;
/// # Attachment
/// A base with optional attachments. /// A base with optional attachments.
/// ///
/// ## Syntax /// ## Syntax
@ -12,58 +11,44 @@ use super::*;
/// $ sum_(i=0)^n a_i = 2^(1+i) $ /// $ sum_(i=0)^n a_i = 2^(1+i) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Attachment
/// - base: `Content` (positional, required) /// Category: math
/// The base to which things are attached. #[node(LayoutMath)]
///
/// - top: `Content` (named)
/// The top attachment.
///
/// - bottom: `Content` (named)
/// The bottom attachment.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct AttachNode { pub struct AttachNode {
/// The base. /// The base to which things are attached.
#[positional]
#[required]
pub base: Content, pub base: Content,
/// The top attachment.
pub top: Option<Content>,
/// The bottom attachment.
pub bottom: Option<Content>,
}
#[node] /// The top attachment.
impl AttachNode { #[named]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let base = args.expect("base")?; pub top: Option<Content>,
let top = args.named("top")?;
let bottom = args.named("bottom")?; /// The bottom attachment.
Ok(Self { base, top, bottom }.pack()) #[named]
} #[default]
pub bottom: Option<Content>,
} }
impl LayoutMath for AttachNode { impl LayoutMath for AttachNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let base = ctx.layout_fragment(&self.base)?; let base = self.base();
let display_limits = base.is::<LimitsNode>();
let display_scripts = base.is::<ScriptsNode>();
let base = ctx.layout_fragment(&base)?;
ctx.style(ctx.style.for_subscript()); ctx.style(ctx.style.for_subscript());
let top = self.top.as_ref().map(|node| ctx.layout_fragment(node)).transpose()?; let top = self.top().map(|node| ctx.layout_fragment(&node)).transpose()?;
ctx.unstyle(); ctx.unstyle();
ctx.style(ctx.style.for_superscript()); ctx.style(ctx.style.for_superscript());
let bottom = self let bottom = self.bottom().map(|node| ctx.layout_fragment(&node)).transpose()?;
.bottom
.as_ref()
.map(|node| ctx.layout_fragment(node))
.transpose()?;
ctx.unstyle(); ctx.unstyle();
let render_limits = self.base.is::<LimitsNode>() let display_limits = display_limits
|| (!self.base.is::<ScriptsNode>() || (!display_scripts
&& ctx.style.size == MathSize::Display && ctx.style.size == MathSize::Display
&& base.class() == Some(MathClass::Large) && base.class() == Some(MathClass::Large)
&& match &base { && match &base {
@ -72,7 +57,7 @@ impl LayoutMath for AttachNode {
_ => false, _ => false,
}); });
if render_limits { if display_limits {
limits(ctx, base, top, bottom) limits(ctx, base, top, bottom)
} else { } else {
scripts(ctx, base, top, bottom) scripts(ctx, base, top, bottom)
@ -80,7 +65,6 @@ impl LayoutMath for AttachNode {
} }
} }
/// # Scripts
/// Force a base to display attachments as scripts. /// Force a base to display attachments as scripts.
/// ///
/// ## Example /// ## Example
@ -88,31 +72,22 @@ impl LayoutMath for AttachNode {
/// $ scripts(sum)_1^2 != sum_1^2 $ /// $ scripts(sum)_1^2 != sum_1^2 $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Scripts
/// - base: `Content` (positional, required) /// Category: math
/// The base to attach the scripts to. #[node(LayoutMath)]
/// pub struct ScriptsNode {
/// ## Category /// The base to attach the scripts to.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub base: Content,
#[derive(Debug, Hash)]
pub struct ScriptsNode(Content);
#[node]
impl ScriptsNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("base")?).pack())
}
} }
impl LayoutMath for ScriptsNode { impl LayoutMath for ScriptsNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
self.0.layout_math(ctx) self.base().layout_math(ctx)
} }
} }
/// # Limits
/// Force a base to display attachments as limits. /// Force a base to display attachments as limits.
/// ///
/// ## Example /// ## Example
@ -120,27 +95,19 @@ impl LayoutMath for ScriptsNode {
/// $ limits(A)_1^2 != A_1^2 $ /// $ limits(A)_1^2 != A_1^2 $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Limits
/// - base: `Content` (positional, required) /// Category: math
/// The base to attach the limits to. #[node(LayoutMath)]
/// pub struct LimitsNode {
/// ## Category /// The base to attach the limits to.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub base: Content,
#[derive(Debug, Hash)]
pub struct LimitsNode(Content);
#[node]
impl LimitsNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("base")?).pack())
}
} }
impl LayoutMath for LimitsNode { impl LayoutMath for LimitsNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
self.0.layout_math(ctx) self.base().layout_math(ctx)
} }
} }

View File

@ -3,7 +3,6 @@ use super::*;
/// How much less high scaled delimiters can be than what they wrap. /// How much less high scaled delimiters can be than what they wrap.
pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// # Left/Right
/// Scales delimiters. /// Scales delimiters.
/// ///
/// While matched delimiters scale by default, this can be used to scale /// While matched delimiters scale by default, this can be used to scale
@ -24,20 +23,22 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// ///
/// Defaults to `{100%}`. /// Defaults to `{100%}`.
/// ///
/// ## Category /// Display: Left/Right
/// math /// Category: math
#[func] #[node(Construct, LayoutMath)]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct LrNode { pub struct LrNode {
/// The delimited content, including the delimiters. /// The delimited content, including the delimiters.
#[positional]
#[required]
pub body: Content, pub body: Content,
/// The size of the brackets. /// The size of the brackets.
pub size: Option<Rel<Length>>, #[named]
#[default]
pub size: Smart<Rel<Length>>,
} }
#[node] impl Construct for LrNode {
impl LrNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let mut body = Content::empty(); let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
@ -46,21 +47,21 @@ impl LrNode {
} }
body += arg; body += arg;
} }
let size = args.named("size")?; let size = args.named::<Smart<Rel<Length>>>("size")?.unwrap_or_default();
Ok(Self { body, size }.pack()) Ok(Self::new(body).with_size(size).pack())
} }
} }
impl LayoutMath for LrNode { impl LayoutMath for LrNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let mut body = &self.body; let mut body = self.body();
if let Some(node) = self.body.to::<LrNode>() { if let Some(node) = body.to::<LrNode>() {
if node.size.is_none() { if node.size().is_auto() {
body = &node.body; body = node.body();
} }
} }
let mut fragments = ctx.layout_fragments(body)?; let mut fragments = ctx.layout_fragments(&body)?;
let axis = scaled!(ctx, axis_height); let axis = scaled!(ctx, axis_height);
let max_extent = fragments let max_extent = fragments
.iter() .iter()
@ -69,7 +70,7 @@ impl LayoutMath for LrNode {
.unwrap_or_default(); .unwrap_or_default();
let height = self let height = self
.size .size()
.unwrap_or(Rel::one()) .unwrap_or(Rel::one())
.resolve(ctx.styles()) .resolve(ctx.styles())
.relative_to(2.0 * max_extent); .relative_to(2.0 * max_extent);
@ -116,7 +117,6 @@ fn scale(
} }
} }
/// # Floor
/// Floor an expression. /// Floor an expression.
/// ///
/// ## Example /// ## Example
@ -128,14 +128,13 @@ fn scale(
/// - body: `Content` (positional, required) /// - body: `Content` (positional, required)
/// The expression to floor. /// The expression to floor.
/// ///
/// ## Category /// Display: Floor
/// math /// Category: math
#[func] #[func]
pub fn floor(args: &mut Args) -> SourceResult<Value> { pub fn floor(args: &mut Args) -> SourceResult<Value> {
delimited(args, '⌊', '⌋') delimited(args, '⌊', '⌋')
} }
/// # Ceil
/// Ceil an expression. /// Ceil an expression.
/// ///
/// ## Example /// ## Example
@ -147,14 +146,13 @@ pub fn floor(args: &mut Args) -> SourceResult<Value> {
/// - body: `Content` (positional, required) /// - body: `Content` (positional, required)
/// The expression to ceil. /// The expression to ceil.
/// ///
/// ## Category /// Display: Ceil
/// math /// Category: math
#[func] #[func]
pub fn ceil(args: &mut Args) -> SourceResult<Value> { pub fn ceil(args: &mut Args) -> SourceResult<Value> {
delimited(args, '⌈', '⌉') delimited(args, '⌈', '⌉')
} }
/// # Abs
/// Take the absolute value of an expression. /// Take the absolute value of an expression.
/// ///
/// ## Example /// ## Example
@ -166,14 +164,13 @@ pub fn ceil(args: &mut Args) -> SourceResult<Value> {
/// - body: `Content` (positional, required) /// - body: `Content` (positional, required)
/// The expression to take the absolute value of. /// The expression to take the absolute value of.
/// ///
/// ## Category /// Display: Abs
/// math /// Category: math
#[func] #[func]
pub fn abs(args: &mut Args) -> SourceResult<Value> { pub fn abs(args: &mut Args) -> SourceResult<Value> {
delimited(args, '|', '|') delimited(args, '|', '|')
} }
/// # Norm
/// Take the norm of an expression. /// Take the norm of an expression.
/// ///
/// ## Example /// ## Example
@ -185,8 +182,8 @@ pub fn abs(args: &mut Args) -> SourceResult<Value> {
/// - body: `Content` (positional, required) /// - body: `Content` (positional, required)
/// The expression to take the norm of. /// The expression to take the norm of.
/// ///
/// ## Category /// Display: Norm
/// math /// Category: math
#[func] #[func]
pub fn norm(args: &mut Args) -> SourceResult<Value> { pub fn norm(args: &mut Args) -> SourceResult<Value> {
delimited(args, '‖', '‖') delimited(args, '‖', '‖')
@ -194,14 +191,11 @@ pub fn norm(args: &mut Args) -> SourceResult<Value> {
fn delimited(args: &mut Args, left: char, right: char) -> SourceResult<Value> { fn delimited(args: &mut Args, left: char, right: char) -> SourceResult<Value> {
Ok(Value::Content( Ok(Value::Content(
LrNode { LrNode::new(Content::sequence(vec![
body: Content::sequence(vec![
TextNode::packed(left), TextNode::packed(left),
args.expect::<Content>("body")?, args.expect::<Content>("body")?,
TextNode::packed(right), TextNode::packed(right),
]), ]))
size: None,
}
.pack(), .pack(),
)) ))
} }

View File

@ -17,41 +17,27 @@ const FRAC_AROUND: Em = Em::new(0.1);
/// expression using round grouping parenthesis. Such parentheses are removed /// expression using round grouping parenthesis. Such parentheses are removed
/// from the output, but you can nest multiple to force them. /// from the output, but you can nest multiple to force them.
/// ///
/// ## Parameters /// Display: Fraction
/// - num: `Content` (positional, required) /// Category: math
/// The fraction's numerator. #[node(LayoutMath)]
///
/// - denom: `Content` (positional, required)
/// The fraction's denominator.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct FracNode { pub struct FracNode {
/// The numerator. /// The fraction's numerator.
#[positional]
#[required]
pub num: Content, pub num: Content,
/// The denominator.
pub denom: Content,
}
#[node] /// The fraction's denominator.
impl FracNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[required]
let num = args.expect("numerator")?; pub denom: Content,
let denom = args.expect("denominator")?;
Ok(Self { num, denom }.pack())
}
} }
impl LayoutMath for FracNode { impl LayoutMath for FracNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.num, &self.denom, false) layout(ctx, &self.num(), &self.denom(), false)
} }
} }
/// # Binomial
/// A binomial expression. /// A binomial expression.
/// ///
/// ## Example /// ## Example
@ -59,37 +45,24 @@ impl LayoutMath for FracNode {
/// $ binom(n, k) $ /// $ binom(n, k) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Binomial
/// - upper: `Content` (positional, required) /// Category: math
/// The binomial's upper index. #[node(LayoutMath)]
///
/// - lower: `Content` (positional, required)
/// The binomial's lower index.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct BinomNode { pub struct BinomNode {
/// The upper index. /// The binomial's upper index.
#[positional]
#[required]
pub upper: Content, pub upper: Content,
/// The lower index.
pub lower: Content,
}
#[node] /// The binomial's lower index.
impl BinomNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[required]
let upper = args.expect("upper index")?; pub lower: Content,
let lower = args.expect("lower index")?;
Ok(Self { upper, lower }.pack())
}
} }
impl LayoutMath for BinomNode { impl LayoutMath for BinomNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.upper, &self.lower, true) layout(ctx, &self.upper(), &self.lower(), true)
} }
} }

View File

@ -4,7 +4,6 @@ const ROW_GAP: Em = Em::new(0.5);
const COL_GAP: Em = Em::new(0.5); const COL_GAP: Em = Em::new(0.5);
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
/// # Vector
/// A column vector. /// A column vector.
/// ///
/// Content in the vector's elements can be aligned with the `&` symbol. /// Content in the vector's elements can be aligned with the `&` symbol.
@ -15,41 +14,33 @@ const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
/// = a + 2b + 3c $ /// = a + 2b + 3c $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Vector
/// - elements: `Content` (positional, variadic) /// Category: math
/// The elements of the vector. #[node(LayoutMath)]
/// pub struct VecNode {
/// ## Category /// The elements of the vector.
/// math #[variadic]
#[func] pub elements: Vec<Content>,
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct VecNode(Vec<Content>);
#[node]
impl VecNode {
/// The delimiter to use. /// The delimiter to use.
/// ///
/// ```example /// ```example
/// #set math.vec(delim: "[") /// #set math.vec(delim: "[")
/// $ vec(1, 2) $ /// $ vec(1, 2) $
/// ``` /// ```
pub const DELIM: Delimiter = Delimiter::Paren; #[settable]
#[default(Delimiter::Paren)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub delim: Delimiter,
Ok(Self(args.all()?).pack())
}
} }
impl LayoutMath for VecNode { impl LayoutMath for VecNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let delim = ctx.styles().get(Self::DELIM); let delim = ctx.styles().get(Self::DELIM);
let frame = layout_vec_body(ctx, &self.0, Align::Center)?; let frame = layout_vec_body(ctx, &self.elements(), Align::Center)?;
layout_delimiters(ctx, frame, Some(delim.open()), Some(delim.close())) layout_delimiters(ctx, frame, Some(delim.open()), Some(delim.close()))
} }
} }
/// # Matrix
/// A matrix. /// A matrix.
/// ///
/// The elements of a row should be separated by commas, while the rows /// The elements of a row should be separated by commas, while the rows
@ -70,33 +61,32 @@ impl LayoutMath for VecNode {
/// ) $ /// ) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Matrix
/// - rows: `Array` (positional, variadic) /// Category: math
/// An array of arrays with the rows of the matrix. #[node(Construct, LayoutMath)]
/// pub struct MatNode {
/// ```example /// An array of arrays with the rows of the matrix.
/// #let data = ((1, 2, 3), (4, 5, 6)) ///
/// #let matrix = math.mat(..data) /// ```example
/// $ v := matrix $ /// #let data = ((1, 2, 3), (4, 5, 6))
/// ``` /// #let matrix = math.mat(..data)
/// /// $ v := matrix $
/// ## Category /// ```
/// math #[variadic]
#[func] pub rows: Vec<Vec<Content>>,
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct MatNode(Vec<Vec<Content>>);
#[node]
impl MatNode {
/// The delimiter to use. /// The delimiter to use.
/// ///
/// ```example /// ```example
/// #set math.mat(delim: "[") /// #set math.mat(delim: "[")
/// $ mat(1, 2; 3, 4) $ /// $ mat(1, 2; 3, 4) $
/// ``` /// ```
pub const DELIM: Delimiter = Delimiter::Paren; #[settable]
#[default(Delimiter::Paren)]
pub delim: Delimiter,
}
impl Construct for MatNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let mut rows = vec![]; let mut rows = vec![];
let mut width = 0; let mut width = 0;
@ -119,19 +109,18 @@ impl MatNode {
} }
} }
Ok(Self(rows).pack()) Ok(Self::new(rows).pack())
} }
} }
impl LayoutMath for MatNode { impl LayoutMath for MatNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let delim = ctx.styles().get(Self::DELIM); let delim = ctx.styles().get(Self::DELIM);
let frame = layout_mat_body(ctx, &self.0)?; let frame = layout_mat_body(ctx, &self.rows())?;
layout_delimiters(ctx, frame, Some(delim.open()), Some(delim.close())) layout_delimiters(ctx, frame, Some(delim.open()), Some(delim.close()))
} }
} }
/// # Cases
/// A case distinction. /// A case distinction.
/// ///
/// Content across different branches can be aligned with the `&` symbol. /// Content across different branches can be aligned with the `&` symbol.
@ -146,36 +135,29 @@ impl LayoutMath for MatNode {
/// ) $ /// ) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Cases
/// - branches: `Content` (positional, variadic) /// Category: math
/// The branches of the case distinction. #[node(LayoutMath)]
/// pub struct CasesNode {
/// ## Category /// The branches of the case distinction.
/// math #[variadic]
#[func] pub branches: Vec<Content>,
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct CasesNode(Vec<Content>);
#[node]
impl CasesNode {
/// The delimiter to use. /// The delimiter to use.
/// ///
/// ```example /// ```example
/// #set math.cases(delim: "[") /// #set math.cases(delim: "[")
/// $ x = cases(1, 2) $ /// $ x = cases(1, 2) $
/// ``` /// ```
pub const DELIM: Delimiter = Delimiter::Brace; #[settable]
#[default(Delimiter::Brace)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub delim: Delimiter,
Ok(Self(args.all()?).pack())
}
} }
impl LayoutMath for CasesNode { impl LayoutMath for CasesNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let delim = ctx.styles().get(Self::DELIM); let delim = ctx.styles().get(Self::DELIM);
let frame = layout_vec_body(ctx, &self.0, Align::Left)?; let frame = layout_vec_body(ctx, &self.branches(), Align::Left)?;
layout_delimiters(ctx, frame, Some(delim.open()), None) layout_delimiters(ctx, frame, Some(delim.open()), None)
} }
} }
@ -214,7 +196,7 @@ impl Delimiter {
} }
} }
castable! { cast_from_value! {
Delimiter, Delimiter,
/// Delimit with parentheses. /// Delimit with parentheses.
"(" => Self::Paren, "(" => Self::Paren,
@ -228,6 +210,16 @@ castable! {
"||" => Self::DoubleBar, "||" => Self::DoubleBar,
} }
cast_to_value! {
v: Delimiter => Value::from(match v {
Delimiter::Paren => "(",
Delimiter::Bracket => "[",
Delimiter::Brace => "{",
Delimiter::Bar => "|",
Delimiter::DoubleBar => "||",
})
}
/// Layout the inner contents of a vector. /// Layout the inner contents of a vector.
fn layout_vec_body( fn layout_vec_body(
ctx: &mut MathContext, ctx: &mut MathContext,

View File

@ -107,7 +107,6 @@ pub fn module() -> Module {
Module::new("math").with_scope(math) Module::new("math").with_scope(math)
} }
/// # Formula
/// A mathematical formula. /// A mathematical formula.
/// ///
/// Can be displayed inline with text or as a separate block. /// Can be displayed inline with text or as a separate block.
@ -132,46 +131,25 @@ pub fn module() -> Module {
/// horizontally. For more details about math syntax, see the /// horizontally. For more details about math syntax, see the
/// [main math page]($category/math). /// [main math page]($category/math).
/// ///
/// ## Parameters /// Display: Formula
/// - body: `Content` (positional, required) /// Category: math
/// The contents of the formula. #[node(Show, Finalize, Layout, LayoutMath)]
///
/// - block: `bool` (named)
/// Whether the formula is displayed as a separate block.
///
/// ## Category
/// math
#[func]
#[capable(Show, Finalize, Layout, LayoutMath)]
#[derive(Debug, Clone, Hash)]
pub struct FormulaNode { pub struct FormulaNode {
/// Whether the formula is displayed as a separate block.
pub block: bool,
/// The content of the formula. /// The content of the formula.
#[positional]
#[required]
pub body: Content, pub body: Content,
}
#[node] /// Whether the formula is displayed as a separate block.
impl FormulaNode { #[named]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default(false)]
let body = args.expect("body")?; pub block: bool,
let block = args.named("block")?.unwrap_or(false);
Ok(Self { block, body }.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.body.clone())),
"block" => Some(Value::Bool(self.block)),
_ => None,
}
}
} }
impl Show for FormulaNode { impl Show for FormulaNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
let mut realized = self.clone().pack().guarded(Guard::Base(NodeId::of::<Self>())); let mut realized = self.clone().pack().guarded(Guard::Base(NodeId::of::<Self>()));
if self.block { if self.block() {
realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) realized = realized.aligned(Axes::with_x(Some(Align::Center.into())))
} }
Ok(realized) Ok(realized)
@ -196,27 +174,29 @@ impl Layout for FormulaNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let block = self.block();
// Find a math font. // Find a math font.
let variant = variant(styles); let variant = variant(styles);
let world = vt.world(); let world = vt.world();
let Some(font) = families(styles) let Some(font) = families(styles)
.find_map(|family| { .find_map(|family| {
let id = world.book().select(family, variant)?; let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?; let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?; let _ = font.ttf().tables().math?.constants?;
Some(font) Some(font)
}) })
else { else {
if let Some(span) = self.body.span() { if let Some(span) = self.span() {
bail!(span, "current font does not support math"); bail!(span, "current font does not support math");
} }
return Ok(Fragment::frame(Frame::new(Size::zero()))) return Ok(Fragment::frame(Frame::new(Size::zero())))
}; };
let mut ctx = MathContext::new(vt, styles, regions, &font, self.block); let mut ctx = MathContext::new(vt, styles, regions, &font, block);
let mut frame = ctx.layout_frame(self)?; let mut frame = ctx.layout_frame(self)?;
if !self.block { if !block {
let slack = styles.get(ParNode::LEADING) * 0.7; let slack = styles.get(ParNode::LEADING) * 0.7;
let top_edge = styles.get(TextNode::TOP_EDGE).resolve(styles, font.metrics()); let top_edge = styles.get(TextNode::TOP_EDGE).resolve(styles, font.metrics());
let bottom_edge = let bottom_edge =
@ -232,38 +212,38 @@ impl Layout for FormulaNode {
} }
} }
#[capability]
pub trait LayoutMath { pub trait LayoutMath {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
} }
impl LayoutMath for FormulaNode { impl LayoutMath for FormulaNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
self.body.layout_math(ctx) self.body().layout_math(ctx)
} }
} }
impl LayoutMath for Content { impl LayoutMath for Content {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
if let Some(node) = self.to::<SequenceNode>() { if let Some(node) = self.to::<SequenceNode>() {
for child in &node.0 { for child in node.children() {
child.layout_math(ctx)?; child.layout_math(ctx)?;
} }
return Ok(()); return Ok(());
} }
if let Some(styled) = self.to::<StyledNode>() { if let Some(styled) = self.to::<StyledNode>() {
if styled.map.contains(TextNode::FAMILY) { let map = styled.map();
if map.contains(TextNode::FAMILY) {
let frame = ctx.layout_content(self)?; let frame = ctx.layout_content(self)?;
ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
return Ok(()); return Ok(());
} }
let prev_map = std::mem::replace(&mut ctx.map, styled.map.clone()); let prev_map = std::mem::replace(&mut ctx.map, map);
let prev_size = ctx.size; let prev_size = ctx.size;
ctx.map.apply(prev_map.clone()); ctx.map.apply(prev_map.clone());
ctx.size = ctx.styles().get(TextNode::SIZE); ctx.size = ctx.styles().get(TextNode::SIZE);
styled.sub.layout_math(ctx)?; styled.sub().layout_math(ctx)?;
ctx.size = prev_size; ctx.size = prev_size;
ctx.map = prev_map; ctx.map = prev_map;
return Ok(()); return Ok(());
@ -280,7 +260,7 @@ impl LayoutMath for Content {
} }
if let Some(node) = self.to::<HNode>() { if let Some(node) = self.to::<HNode>() {
if let Spacing::Rel(rel) = node.amount { if let Spacing::Rel(rel) = node.amount() {
if rel.rel.is_zero() { if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles())));
} }
@ -289,7 +269,7 @@ impl LayoutMath for Content {
} }
if let Some(node) = self.to::<TextNode>() { if let Some(node) = self.to::<TextNode>() {
ctx.layout_text(&node.0)?; ctx.layout_text(&node.text())?;
return Ok(()); return Ok(());
} }

View File

@ -2,7 +2,6 @@ use typst::eval::Scope;
use super::*; use super::*;
/// # Text Operator
/// A text operator in a math formula. /// A text operator in a math formula.
/// ///
/// ## Example /// ## Example
@ -19,45 +18,30 @@ use super::*;
/// `max`, `min`, `Pr`, `sec`, `sin`, `sinh`, `sup`, `tan`, `tg`, `tanh`, /// `max`, `min`, `Pr`, `sec`, `sin`, `sinh`, `sup`, `tan`, `tg`, `tanh`,
/// `liminf`, and `limsup`. /// `liminf`, and `limsup`.
/// ///
/// ## Parameters /// Display: Text Operator
/// - text: `EcoString` (positional, required) /// Category: math
/// The operator's text. #[node(LayoutMath)]
///
/// - limits: `bool` (named)
/// Whether the operator should force attachments to display as limits.
///
/// Defaults to `{false}`.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OpNode { pub struct OpNode {
/// The operator's text. /// The operator's text.
#[positional]
#[required]
pub text: EcoString, pub text: EcoString,
/// Whether the operator should force attachments to display as limits.
pub limits: bool,
}
#[node] /// Whether the operator should force attachments to display as limits.
impl OpNode { ///
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { /// Defaults to `{false}`.
Ok(Self { #[named]
text: args.expect("text")?, #[default(false)]
limits: args.named("limits")?.unwrap_or(false), pub limits: bool,
}
.pack())
}
} }
impl LayoutMath for OpNode { impl LayoutMath for OpNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let frame = ctx.layout_content(&TextNode(self.text.clone()).pack())?; let frame = ctx.layout_content(&TextNode::packed(self.text()))?;
ctx.push( ctx.push(
FrameFragment::new(ctx, frame) FrameFragment::new(ctx, frame)
.with_class(MathClass::Large) .with_class(MathClass::Large)
.with_limits(self.limits), .with_limits(self.limits()),
); );
Ok(()) Ok(())
} }
@ -68,13 +52,15 @@ macro_rules! ops {
pub(super) fn define(math: &mut Scope) { pub(super) fn define(math: &mut Scope) {
$(math.define( $(math.define(
stringify!($name), stringify!($name),
OpNode { OpNode::new(ops!(@name $name $(: $value)?).into())
text: ops!(@name $name $(: $value)?).into(), .with_limits(ops!(@limit $($tts)*))
limits: ops!(@limit $($tts)*), .pack()
}.pack()
);)* );)*
let dif = |d| HNode::strong(THIN).pack() + UprightNode(TextNode::packed(d)).pack(); let dif = |d| {
HNode::new(THIN.into()).pack()
+ UprightNode::new(TextNode::packed(d)).pack()
};
math.define("dif", dif('d')); math.define("dif", dif('d'));
math.define("Dif", dif('D')); math.define("Dif", dif('D'));
} }

View File

@ -1,6 +1,5 @@
use super::*; use super::*;
/// # Square Root
/// A square root. /// A square root.
/// ///
/// ## Example /// ## Example
@ -8,31 +7,22 @@ use super::*;
/// $ sqrt(x^2) = x = sqrt(x)^2 $ /// $ sqrt(x^2) = x = sqrt(x)^2 $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Square Root
/// - radicand: `Content` (positional, required) /// Category: math
/// The expression to take the square root of. #[node(LayoutMath)]
/// pub struct SqrtNode {
/// ## Category /// The expression to take the square root of.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub radicand: Content,
#[derive(Debug, Hash)]
pub struct SqrtNode(pub Content);
#[node]
impl SqrtNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("radicand")?).pack())
}
} }
impl LayoutMath for SqrtNode { impl LayoutMath for SqrtNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, None, &self.0) layout(ctx, None, &self.radicand())
} }
} }
/// # Root
/// A general root. /// A general root.
/// ///
/// ## Example /// ## Example
@ -40,37 +30,24 @@ impl LayoutMath for SqrtNode {
/// $ root(3, x) $ /// $ root(3, x) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Root
/// - index: `Content` (positional, required) /// Category: math
/// Which root of the radicand to take. #[node(LayoutMath)]
///
/// - radicand: `Content` (positional, required)
/// The expression to take the root of.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct RootNode { pub struct RootNode {
/// Which root of the radicand to take.
#[positional]
#[required]
index: Content, index: Content,
radicand: Content,
}
#[node] /// The expression to take the root of.
impl RootNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[required]
Ok(Self { radicand: Content,
index: args.expect("index")?,
radicand: args.expect("radicand")?,
}
.pack())
}
} }
impl LayoutMath for RootNode { impl LayoutMath for RootNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, Some(&self.index), &self.radicand) layout(ctx, Some(&self.index()), &self.radicand())
} }
} }
@ -164,7 +141,7 @@ fn layout(
/// Select a precomposed radical, if the font has it. /// Select a precomposed radical, if the font has it.
fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> { fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> {
let node = index?.to::<TextNode>()?; let node = index?.to::<TextNode>()?;
let c = match node.0.as_str() { let c = match node.text().as_str() {
"3" => '∛', "3" => '∛',
"4" => '∜', "4" => '∜',
_ => return None, _ => return None,

View File

@ -103,7 +103,7 @@ impl MathRow {
pub fn to_frame(self, ctx: &MathContext) -> Frame { pub fn to_frame(self, ctx: &MathContext) -> Frame {
let styles = ctx.styles(); let styles = ctx.styles();
let align = styles.get(AlignNode::ALIGNS).x.resolve(styles); let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles);
self.to_aligned_frame(ctx, &[], align) self.to_aligned_frame(ctx, &[], align)
} }
@ -200,10 +200,7 @@ impl MathRow {
} }
} }
impl<T> From<T> for MathRow impl<T: Into<MathFragment>> From<T> for MathRow {
where
T: Into<MathFragment>,
{
fn from(fragment: T) -> Self { fn from(fragment: T) -> Self {
Self(vec![fragment.into()]) Self(vec![fragment.into()])
} }

View File

@ -7,10 +7,10 @@ pub(super) const QUAD: Em = Em::new(1.0);
/// Hook up all spacings. /// Hook up all spacings.
pub(super) fn define(math: &mut Scope) { pub(super) fn define(math: &mut Scope) {
math.define("thin", HNode::strong(THIN).pack()); math.define("thin", HNode::new(THIN.into()).pack());
math.define("med", HNode::strong(MEDIUM).pack()); math.define("med", HNode::new(MEDIUM.into()).pack());
math.define("thick", HNode::strong(THICK).pack()); math.define("thick", HNode::new(THICK.into()).pack());
math.define("quad", HNode::strong(QUAD).pack()); math.define("quad", HNode::new(QUAD.into()).pack());
} }
/// Create the spacing between two fragments in a given style. /// Create the spacing between two fragments in a given style.

View File

@ -1,6 +1,5 @@
use super::*; use super::*;
/// # Bold
/// Bold font style in math. /// Bold font style in math.
/// ///
/// ## Example /// ## Example
@ -8,34 +7,25 @@ use super::*;
/// $ bold(A) := B^+ $ /// $ bold(A) := B^+ $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Bold
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct BoldNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct BoldNode(pub Content);
#[node]
impl BoldNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for BoldNode { impl LayoutMath for BoldNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_bold(true)); ctx.style(ctx.style.with_bold(true));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Upright
/// Upright (non-italic) font style in math. /// Upright (non-italic) font style in math.
/// ///
/// ## Example /// ## Example
@ -43,98 +33,71 @@ impl LayoutMath for BoldNode {
/// $ upright(A) != A $ /// $ upright(A) != A $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Upright
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct UprightNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct UprightNode(pub Content);
#[node]
impl UprightNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for UprightNode { impl LayoutMath for UprightNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_italic(false)); ctx.style(ctx.style.with_italic(false));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Italic
/// Italic font style in math. /// Italic font style in math.
/// ///
/// For roman letters and greek lowercase letters, this is already the default. /// For roman letters and greek lowercase letters, this is already the default.
/// ///
/// ## Parameters /// Display: Italic
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct ItalicNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct ItalicNode(pub Content);
#[node]
impl ItalicNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for ItalicNode { impl LayoutMath for ItalicNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_italic(true)); ctx.style(ctx.style.with_italic(true));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Serif
/// Serif (roman) font style in math. /// Serif (roman) font style in math.
/// ///
/// This is already the default. /// This is already the default.
/// ///
/// ## Parameters /// Display: Serif
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct SerifNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct SerifNode(pub Content);
#[node]
impl SerifNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for SerifNode { impl LayoutMath for SerifNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Serif)); ctx.style(ctx.style.with_variant(MathVariant::Serif));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Sans-serif
/// Sans-serif font style in math. /// Sans-serif font style in math.
/// ///
/// ## Example /// ## Example
@ -142,34 +105,25 @@ impl LayoutMath for SerifNode {
/// $ sans(A B C) $ /// $ sans(A B C) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Sans-serif
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct SansNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct SansNode(pub Content);
#[node]
impl SansNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for SansNode { impl LayoutMath for SansNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Sans)); ctx.style(ctx.style.with_variant(MathVariant::Sans));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Calligraphic
/// Calligraphic font style in math. /// Calligraphic font style in math.
/// ///
/// ## Example /// ## Example
@ -177,34 +131,25 @@ impl LayoutMath for SansNode {
/// Let $cal(P)$ be the set of ... /// Let $cal(P)$ be the set of ...
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Calligraphic
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct CalNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct CalNode(pub Content);
#[node]
impl CalNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for CalNode { impl LayoutMath for CalNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Cal)); ctx.style(ctx.style.with_variant(MathVariant::Cal));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Fraktur
/// Fraktur font style in math. /// Fraktur font style in math.
/// ///
/// ## Example /// ## Example
@ -212,34 +157,25 @@ impl LayoutMath for CalNode {
/// $ frak(P) $ /// $ frak(P) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Fraktur
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct FrakNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct FrakNode(pub Content);
#[node]
impl FrakNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for FrakNode { impl LayoutMath for FrakNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Frak)); ctx.style(ctx.style.with_variant(MathVariant::Frak));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Monospace
/// Monospace font style in math. /// Monospace font style in math.
/// ///
/// ## Example /// ## Example
@ -247,34 +183,25 @@ impl LayoutMath for FrakNode {
/// $ mono(x + y = z) $ /// $ mono(x + y = z) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Monospace
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct MonoNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct MonoNode(pub Content);
#[node]
impl MonoNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for MonoNode { impl LayoutMath for MonoNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Mono)); ctx.style(ctx.style.with_variant(MathVariant::Mono));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }
} }
/// # Blackboard Bold
/// Blackboard bold (double-struck) font style in math. /// Blackboard bold (double-struck) font style in math.
/// ///
/// For uppercase latin letters, blackboard bold is additionally available /// For uppercase latin letters, blackboard bold is additionally available
@ -287,28 +214,20 @@ impl LayoutMath for MonoNode {
/// $ f: NN -> RR $ /// $ f: NN -> RR $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Blackboard Bold
/// - body: `Content` (positional, required) /// Category: math
/// The piece of formula to style. #[node(LayoutMath)]
/// pub struct BbNode {
/// ## Category /// The piece of formula to style.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct BbNode(pub Content);
#[node]
impl BbNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for BbNode { impl LayoutMath for BbNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_variant(MathVariant::Bb)); ctx.style(ctx.style.with_variant(MathVariant::Bb));
self.0.layout_math(ctx)?; self.body().layout_math(ctx)?;
ctx.unstyle(); ctx.unstyle();
Ok(()) Ok(())
} }

View File

@ -4,7 +4,6 @@ const LINE_GAP: Em = Em::new(0.15);
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
const BRACKET_GAP: Em = Em::new(0.25); const BRACKET_GAP: Em = Em::new(0.25);
/// # Underline
/// A horizontal line under content. /// A horizontal line under content.
/// ///
/// ## Example /// ## Example
@ -12,31 +11,22 @@ const BRACKET_GAP: Em = Em::new(0.25);
/// $ underline(1 + 2 + ... + 5) $ /// $ underline(1 + 2 + ... + 5) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Underline
/// - body: `Content` (positional, required) /// Category: math
/// The content above the line. #[node(LayoutMath)]
/// pub struct UnderlineNode {
/// ## Category /// The content above the line.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct UnderlineNode(Content);
#[node]
impl UnderlineNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for UnderlineNode { impl LayoutMath for UnderlineNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.0, &None, '\u{305}', LINE_GAP, false) layout(ctx, &self.body(), &None, '\u{305}', LINE_GAP, false)
} }
} }
/// # Overline
/// A horizontal line over content. /// A horizontal line over content.
/// ///
/// ## Example /// ## Example
@ -44,31 +34,22 @@ impl LayoutMath for UnderlineNode {
/// $ overline(1 + 2 + ... + 5) $ /// $ overline(1 + 2 + ... + 5) $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Overline
/// - body: `Content` (positional, required) /// Category: math
/// The content below the line. #[node(LayoutMath)]
/// pub struct OverlineNode {
/// ## Category /// The content below the line.
/// math #[positional]
#[func] #[required]
#[capable(LayoutMath)] pub body: Content,
#[derive(Debug, Hash)]
pub struct OverlineNode(Content);
#[node]
impl OverlineNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
} }
impl LayoutMath for OverlineNode { impl LayoutMath for OverlineNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.0, &None, '\u{332}', LINE_GAP, true) layout(ctx, &self.body(), &None, '\u{332}', LINE_GAP, true)
} }
} }
/// # Underbrace
/// A horizontal brace under content, with an optional annotation below. /// A horizontal brace under content, with an optional annotation below.
/// ///
/// ## Example /// ## Example
@ -76,41 +57,27 @@ impl LayoutMath for OverlineNode {
/// $ underbrace(1 + 2 + ... + 5, "numbers") $ /// $ underbrace(1 + 2 + ... + 5, "numbers") $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Underbrace
/// - body: `Content` (positional, required) /// Category: math
/// The content above the brace. #[node(LayoutMath)]
///
/// - annotation: `Content` (positional)
/// The optional content below the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderbraceNode { pub struct UnderbraceNode {
/// The content above the brace. /// The content above the brace.
#[positional]
#[required]
pub body: Content, pub body: Content,
/// The optional content below the brace.
pub annotation: Option<Content>,
}
#[node] /// The optional content below the brace.
impl UnderbraceNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let body = args.expect("body")?; pub annotation: Option<Content>,
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
} }
impl LayoutMath for UnderbraceNode { impl LayoutMath for UnderbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⏟', BRACE_GAP, false) layout(ctx, &self.body(), &self.annotation(), '⏟', BRACE_GAP, false)
} }
} }
/// # Overbrace
/// A horizontal brace over content, with an optional annotation above. /// A horizontal brace over content, with an optional annotation above.
/// ///
/// ## Example /// ## Example
@ -118,41 +85,27 @@ impl LayoutMath for UnderbraceNode {
/// $ overbrace(1 + 2 + ... + 5, "numbers") $ /// $ overbrace(1 + 2 + ... + 5, "numbers") $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Overbrace
/// - body: `Content` (positional, required) /// Category: math
/// The content below the brace. #[node(LayoutMath)]
///
/// - annotation: `Content` (positional)
/// The optional content above the brace.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverbraceNode { pub struct OverbraceNode {
/// The content below the brace. /// The content below the brace.
#[positional]
#[required]
pub body: Content, pub body: Content,
/// The optional content above the brace.
pub annotation: Option<Content>,
}
#[node] /// The optional content above the brace.
impl OverbraceNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let body = args.expect("body")?; pub annotation: Option<Content>,
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
} }
impl LayoutMath for OverbraceNode { impl LayoutMath for OverbraceNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⏞', BRACE_GAP, true) layout(ctx, &self.body(), &self.annotation(), '⏞', BRACE_GAP, true)
} }
} }
/// # Underbracket
/// A horizontal bracket under content, with an optional annotation below. /// A horizontal bracket under content, with an optional annotation below.
/// ///
/// ## Example /// ## Example
@ -160,41 +113,27 @@ impl LayoutMath for OverbraceNode {
/// $ underbracket(1 + 2 + ... + 5, "numbers") $ /// $ underbracket(1 + 2 + ... + 5, "numbers") $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Underbracket
/// - body: `Content` (positional, required) /// Category: math
/// The content above the bracket. #[node(LayoutMath)]
///
/// - annotation: `Content` (positional)
/// The optional content below the bracket.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct UnderbracketNode { pub struct UnderbracketNode {
/// The content above the bracket. /// The content above the bracket.
#[positional]
#[required]
pub body: Content, pub body: Content,
/// The optional content below the bracket.
pub annotation: Option<Content>,
}
#[node] /// The optional content below the bracket.
impl UnderbracketNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let body = args.expect("body")?; pub annotation: Option<Content>,
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
} }
impl LayoutMath for UnderbracketNode { impl LayoutMath for UnderbracketNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⎵', BRACKET_GAP, false) layout(ctx, &self.body(), &self.annotation(), '⎵', BRACKET_GAP, false)
} }
} }
/// # Overbracket
/// A horizontal bracket over content, with an optional annotation above. /// A horizontal bracket over content, with an optional annotation above.
/// ///
/// ## Example /// ## Example
@ -202,37 +141,24 @@ impl LayoutMath for UnderbracketNode {
/// $ overbracket(1 + 2 + ... + 5, "numbers") $ /// $ overbracket(1 + 2 + ... + 5, "numbers") $
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Overbracket
/// - body: `Content` (positional, required) /// Category: math
/// The content below the bracket. #[node(LayoutMath)]
///
/// - annotation: `Content` (positional)
/// The optional content above the bracket.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct OverbracketNode { pub struct OverbracketNode {
/// The content below the bracket. /// The content below the bracket.
#[positional]
#[required]
pub body: Content, pub body: Content,
/// The optional content above the bracket.
pub annotation: Option<Content>,
}
#[node] /// The optional content above the bracket.
impl OverbracketNode { #[positional]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let body = args.expect("body")?; pub annotation: Option<Content>,
let annotation = args.eat()?;
Ok(Self { body, annotation }.pack())
}
} }
impl LayoutMath for OverbracketNode { impl LayoutMath for OverbracketNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
layout(ctx, &self.body, &self.annotation, '⎴', BRACKET_GAP, true) layout(ctx, &self.body(), &self.annotation(), '⎴', BRACKET_GAP, true)
} }
} }

View File

@ -1,7 +1,8 @@
use typst::model::StyledNode;
use crate::layout::{LayoutRoot, PageNode}; use crate::layout::{LayoutRoot, PageNode};
use crate::prelude::*; use crate::prelude::*;
/// # Document
/// The root element of a document and its metadata. /// The root element of a document and its metadata.
/// ///
/// All documents are automatically wrapped in a `document` element. The main /// All documents are automatically wrapped in a `document` element. The main
@ -11,33 +12,48 @@ use crate::prelude::*;
/// The metadata set with this function is not rendered within the document. /// The metadata set with this function is not rendered within the document.
/// Instead, it is embedded in the compiled PDF file. /// Instead, it is embedded in the compiled PDF file.
/// ///
/// ## Category /// Display: Document
/// meta /// Category: meta
#[func] #[node(LayoutRoot)]
#[capable(LayoutRoot)] pub struct DocumentNode {
#[derive(Hash)] /// The page runs.
pub struct DocumentNode(pub StyleVec<PageNode>); #[variadic]
pub children: Vec<Content>,
#[node]
impl DocumentNode {
/// The document's title. This is often rendered as the title of the /// The document's title. This is often rendered as the title of the
/// PDF viewer window. /// PDF viewer window.
#[property(referenced)] #[settable]
pub const TITLE: Option<EcoString> = None; #[default]
pub title: Option<EcoString>,
/// The document's authors. /// The document's authors.
#[property(referenced)] #[settable]
pub const AUTHOR: Author = Author(vec![]); #[default]
pub author: Author,
} }
impl LayoutRoot for DocumentNode { impl LayoutRoot for DocumentNode {
/// Layout the document into a sequence of frames, one per page. /// Layout the document into a sequence of frames, one per page.
fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
let mut pages = vec![]; let mut pages = vec![];
for (page, map) in self.0.iter() {
for mut child in self.children() {
let map;
let outer = styles;
let mut styles = outer;
if let Some(node) = child.to::<StyledNode>() {
map = node.map();
styles = outer.chain(&map);
child = node.sub();
}
if let Some(page) = child.to::<PageNode>() {
let number = 1 + pages.len(); let number = 1 + pages.len();
let fragment = page.layout(vt, number, styles.chain(map))?; let fragment = page.layout(vt, number, styles)?;
pages.extend(fragment); pages.extend(fragment);
} else if let Some(span) = child.span() {
bail!(span, "unexpected document child");
}
} }
Ok(Document { Ok(Document {
@ -48,19 +64,16 @@ impl LayoutRoot for DocumentNode {
} }
} }
impl Debug for DocumentNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Document ")?;
self.0.fmt(f)
}
}
/// A list of authors. /// A list of authors.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Default, Clone, Hash)]
pub struct Author(Vec<EcoString>); pub struct Author(Vec<EcoString>);
castable! { cast_from_value! {
Author, Author,
v: EcoString => Self(vec![v]), v: EcoString => Self(vec![v]),
v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
} }
cast_to_value! {
v: Author => v.0.into()
}

View File

@ -5,7 +5,6 @@ use crate::layout::{BlockNode, HNode, VNode};
use crate::prelude::*; use crate::prelude::*;
use crate::text::{TextNode, TextSize}; use crate::text::{TextNode, TextSize};
/// # Heading
/// A section heading. /// A section heading.
/// ///
/// With headings, you can structure your document into sections. Each heading /// With headings, you can structure your document into sections. Each heading
@ -39,28 +38,20 @@ use crate::text::{TextNode, TextSize};
/// one or multiple equals signs, followed by a space. The number of equals /// one or multiple equals signs, followed by a space. The number of equals
/// signs determines the heading's logical nesting depth. /// signs determines the heading's logical nesting depth.
/// ///
/// ## Parameters /// Display: Heading
/// - title: `Content` (positional, required) /// Category: meta
/// The heading's title. #[node(Prepare, Show, Finalize)]
///
/// - level: `NonZeroUsize` (named)
/// The logical nesting depth of the heading, starting from one.
///
/// ## Category
/// meta
#[func]
#[capable(Prepare, Show, Finalize)]
#[derive(Debug, Hash)]
pub struct HeadingNode { pub struct HeadingNode {
/// The logical nesting depth of the section, starting from one. In the /// The heading's title.
/// default style, this controls the text size of the heading. #[positional]
pub level: NonZeroUsize, #[required]
/// The heading's contents.
pub title: Content, pub title: Content,
}
#[node] /// The logical nesting depth of the heading, starting from one.
impl HeadingNode { #[named]
#[default(NonZeroUsize::new(1).unwrap())]
pub level: NonZeroUsize,
/// How to number the heading. Accepts a /// How to number the heading. Accepts a
/// [numbering pattern or function]($func/numbering). /// [numbering pattern or function]($func/numbering).
/// ///
@ -71,8 +62,9 @@ impl HeadingNode {
/// == A subsection /// == A subsection
/// === A sub-subsection /// === A sub-subsection
/// ``` /// ```
#[property(referenced)] #[settable]
pub const NUMBERING: Option<Numbering> = None; #[default]
pub numbering: Option<Numbering>,
/// Whether the heading should appear in the outline. /// Whether the heading should appear in the outline.
/// ///
@ -86,23 +78,9 @@ impl HeadingNode {
/// This heading does not appear /// This heading does not appear
/// in the outline. /// in the outline.
/// ``` /// ```
pub const OUTLINED: bool = true; #[settable]
#[default(true)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub outlined: bool,
Ok(Self {
title: args.expect("title")?,
level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()),
}
.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"level" => Some(Value::Int(self.level.get() as i64)),
"title" => Some(Value::Content(self.title.clone())),
_ => None,
}
}
} }
impl Prepare for HeadingNode { impl Prepare for HeadingNode {
@ -121,7 +99,7 @@ impl Prepare for HeadingNode {
} }
let numbers = node.field("numbers").unwrap(); let numbers = node.field("numbers").unwrap();
if numbers != Value::None { if *numbers != Value::None {
let heading = node.to::<Self>().unwrap(); let heading = node.to::<Self>().unwrap();
counter.advance(heading); counter.advance(heading);
} }
@ -136,38 +114,34 @@ impl Prepare for HeadingNode {
this.push_field("numbers", numbers); this.push_field("numbers", numbers);
let meta = Meta::Node(my_id, this.clone()); let meta = Meta::Node(my_id, this.clone());
Ok(this.styled(Meta::DATA, vec![meta])) Ok(this.styled(MetaNode::DATA, vec![meta]))
} }
} }
impl Show for HeadingNode { impl Show for HeadingNode {
fn show(&self, _: &mut Vt, this: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, this: &Content, _: StyleChain) -> SourceResult<Content> {
let mut realized = self.title.clone(); let mut realized = self.title();
let numbers = this.field("numbers").unwrap(); let numbers = this.field("numbers").unwrap();
if numbers != Value::None { if *numbers != Value::None {
realized = numbers.display() realized = numbers.clone().display()
+ HNode { amount: Em::new(0.3).into(), weak: true }.pack() + HNode::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized; + realized;
} }
Ok(BlockNode { Ok(BlockNode::new().with_body(realized).pack())
body: realized,
width: Smart::Auto,
height: Smart::Auto,
}
.pack())
} }
} }
impl Finalize for HeadingNode { impl Finalize for HeadingNode {
fn finalize(&self, realized: Content) -> Content { fn finalize(&self, realized: Content) -> Content {
let scale = match self.level.get() { let level = self.level().get();
let scale = match level {
1 => 1.4, 1 => 1.4,
2 => 1.2, 2 => 1.2,
_ => 1.0, _ => 1.0,
}; };
let size = Em::new(scale); let size = Em::new(scale);
let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
let below = Em::new(0.75) / scale; let below = Em::new(0.75) / scale;
let mut map = StyleMap::new(); let mut map = StyleMap::new();
@ -191,7 +165,7 @@ impl HeadingCounter {
/// Advance the counter and return the numbers for the given heading. /// Advance the counter and return the numbers for the given heading.
pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] { pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] {
let level = heading.level.get(); let level = heading.level().get();
if self.0.len() >= level { if self.0.len() >= level {
self.0[level - 1] = self.0[level - 1].saturating_add(1); self.0[level - 1] = self.0[level - 1].saturating_add(1);

View File

@ -1,7 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::text::{Hyphenate, TextNode}; use crate::text::{Hyphenate, TextNode};
/// # Link
/// Link to a URL or another location in the document. /// Link to a URL or another location in the document.
/// ///
/// The link function makes its positional `body` argument clickable and links /// The link function makes its positional `body` argument clickable and links
@ -50,67 +49,62 @@ use crate::text::{Hyphenate, TextNode};
/// The content that should become a link. If `dest` is an URL string, the /// The content that should become a link. If `dest` is an URL string, the
/// parameter can be omitted. In this case, the URL will be shown as the link. /// parameter can be omitted. In this case, the URL will be shown as the link.
/// ///
/// ## Category /// Display: Link
/// meta /// Category: meta
#[func] #[node(Construct, Show, Finalize)]
#[capable(Show, Finalize)]
#[derive(Debug, Hash)]
pub struct LinkNode { pub struct LinkNode {
/// The destination the link points to. /// The destination the link points to.
#[positional]
#[required]
pub dest: Destination, pub dest: Destination,
/// How the link is represented. /// How the link is represented.
#[positional]
#[default]
pub body: Content, pub body: Content,
} }
impl LinkNode { impl LinkNode {
/// Create a link node from a URL with its bare text. /// Create a link node from a URL with its bare text.
pub fn from_url(url: EcoString) -> Self { pub fn from_url(url: EcoString) -> Self {
let mut text = url.as_str(); let body = body_from_url(&url);
for prefix in ["mailto:", "tel:"] { Self::new(Destination::Url(url)).with_body(body)
text = text.trim_start_matches(prefix);
}
let shorter = text.len() < url.len();
let body = TextNode::packed(if shorter { text.into() } else { url.clone() });
Self { dest: Destination::Url(url), body }
} }
} }
#[node] impl Construct for LinkNode {
impl LinkNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let dest = args.expect::<Destination>("destination")?; let dest = args.expect::<Destination>("destination")?;
Ok(match dest { let body = match &dest {
Destination::Url(url) => match args.eat()? { Destination::Url(url) => match args.eat()? {
Some(body) => Self { dest: Destination::Url(url), body }, Some(body) => body,
None => Self::from_url(url), None => body_from_url(url),
}, },
Destination::Internal(_) => Self { dest, body: args.expect("body")? }, Destination::Internal(_) => args.expect("body")?,
} };
.pack()) Ok(Self::new(dest).with_body(body).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"dest" => Some(match &self.dest {
Destination::Url(url) => Value::Str(url.clone().into()),
Destination::Internal(loc) => Value::Dict(loc.encode()),
}),
"body" => Some(Value::Content(self.body.clone())),
_ => None,
}
} }
} }
impl Show for LinkNode { impl Show for LinkNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone()) Ok(self.body())
} }
} }
impl Finalize for LinkNode { impl Finalize for LinkNode {
fn finalize(&self, realized: Content) -> Content { fn finalize(&self, realized: Content) -> Content {
realized realized
.styled(Meta::DATA, vec![Meta::Link(self.dest.clone())]) .styled(MetaNode::DATA, vec![Meta::Link(self.dest())])
.styled(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false))) .styled(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false)))
} }
} }
fn body_from_url(url: &EcoString) -> Content {
let mut text = url.as_str();
for prefix in ["mailto:", "tel:"] {
text = text.trim_start_matches(prefix);
}
let shorter = text.len() < url.len();
TextNode::packed(if shorter { text.into() } else { url.clone() })
}

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use crate::prelude::*; use crate::prelude::*;
use crate::text::Case; use crate::text::Case;
/// # Numbering
/// Apply a numbering to a sequence of numbers. /// Apply a numbering to a sequence of numbers.
/// ///
/// A numbering defines how a sequence of numbers should be displayed as /// A numbering defines how a sequence of numbers should be displayed as
@ -61,8 +60,8 @@ use crate::text::Case;
/// ///
/// - returns: any /// - returns: any
/// ///
/// ## Category /// Display: Numbering
/// meta /// Category: meta
#[func] #[func]
pub fn numbering(vm: &Vm, args: &mut Args) -> SourceResult<Value> { pub fn numbering(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let numbering = args.expect::<Numbering>("pattern or function")?; let numbering = args.expect::<Numbering>("pattern or function")?;
@ -99,12 +98,19 @@ impl Numbering {
} }
} }
castable! { cast_from_value! {
Numbering, Numbering,
v: Str => Self::Pattern(v.parse()?), v: NumberingPattern => Self::Pattern(v),
v: Func => Self::Func(v), v: Func => Self::Func(v),
} }
cast_to_value! {
v: Numbering => match v {
Numbering::Pattern(pattern) => pattern.into(),
Numbering::Func(func) => func.into(),
}
}
/// How to turn a number into text. /// How to turn a number into text.
/// ///
/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, `I` /// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, `I`
@ -173,12 +179,8 @@ impl FromStr for NumberingPattern {
let mut handled = 0; let mut handled = 0;
for (i, c) in pattern.char_indices() { for (i, c) in pattern.char_indices() {
let kind = match c.to_ascii_lowercase() { let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else {
'1' => NumberingKind::Arabic, continue;
'a' => NumberingKind::Letter,
'i' => NumberingKind::Roman,
'*' => NumberingKind::Symbol,
_ => continue,
}; };
let prefix = pattern[handled..i].into(); let prefix = pattern[handled..i].into();
@ -196,6 +198,27 @@ impl FromStr for NumberingPattern {
} }
} }
cast_from_value! {
NumberingPattern,
v: Str => v.parse()?,
}
cast_to_value! {
v: NumberingPattern => {
let mut pat = EcoString::new();
for (prefix, kind, case) in &v.pieces {
pat.push_str(prefix);
let mut c = kind.to_char();
if *case == Case::Upper {
c = c.to_ascii_uppercase();
}
pat.push(c);
}
pat.push_str(&v.suffix);
pat.into()
}
}
/// Different kinds of numberings. /// Different kinds of numberings.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum NumberingKind { enum NumberingKind {
@ -206,6 +229,27 @@ enum NumberingKind {
} }
impl NumberingKind { impl NumberingKind {
/// Create a numbering kind from a lowercase character.
pub fn from_char(c: char) -> Option<Self> {
Some(match c {
'1' => NumberingKind::Arabic,
'a' => NumberingKind::Letter,
'i' => NumberingKind::Roman,
'*' => NumberingKind::Symbol,
_ => return None,
})
}
/// The lowercase character for this numbering kind.
pub fn to_char(self) -> char {
match self {
Self::Arabic => '1',
Self::Letter => 'a',
Self::Roman => 'i',
Self::Symbol => '*',
}
}
/// Apply the numbering to the given number. /// Apply the numbering to the given number.
pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString { pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString {
let mut n = n.get(); let mut n = n.get();

View File

@ -1,11 +1,8 @@
use super::HeadingNode; use super::HeadingNode;
use crate::layout::{ use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode};
BoxNode, HNode, HideNode, ParbreakNode, RepeatNode, Sizing, Spacing,
};
use crate::prelude::*; use crate::prelude::*;
use crate::text::{LinebreakNode, SpaceNode, TextNode}; use crate::text::{LinebreakNode, SpaceNode, TextNode};
/// # Outline
/// A section outline / table of contents. /// A section outline / table of contents.
/// ///
/// This function generates a list of all headings in the document, up to a /// This function generates a list of all headings in the document, up to a
@ -23,27 +20,25 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode};
/// #lorem(10) /// #lorem(10)
/// ``` /// ```
/// ///
/// ## Category /// Display: Outline
/// meta /// Category: meta
#[func] #[node(Prepare, Show)]
#[capable(Prepare, Show)] pub struct OutlineNode {
#[derive(Debug, Hash)]
pub struct OutlineNode;
#[node]
impl OutlineNode {
/// The title of the outline. /// The title of the outline.
/// ///
/// - When set to `{auto}`, an appropriate title for the [text /// - When set to `{auto}`, an appropriate title for the [text
/// language]($func/text.lang) will be used. This is the default. /// language]($func/text.lang) will be used. This is the default.
/// - When set to `{none}`, the outline will not have a title. /// - When set to `{none}`, the outline will not have a title.
/// - A custom title can be set by passing content. /// - A custom title can be set by passing content.
#[property(referenced)] #[settable]
pub const TITLE: Option<Smart<Content>> = Some(Smart::Auto); #[default(Some(Smart::Auto))]
pub title: Option<Smart<Content>>,
/// The maximum depth up to which headings are included in the outline. When /// The maximum depth up to which headings are included in the outline. When
/// this argument is `{none}`, all headings are included. /// this argument is `{none}`, all headings are included.
pub const DEPTH: Option<NonZeroUsize> = None; #[settable]
#[default]
pub depth: Option<NonZeroUsize>,
/// Whether to indent the subheadings to align the start of their numbering /// Whether to indent the subheadings to align the start of their numbering
/// with the title of their parents. This will only have an effect if a /// with the title of their parents. This will only have an effect if a
@ -62,7 +57,9 @@ impl OutlineNode {
/// == Products /// == Products
/// #lorem(10) /// #lorem(10)
/// ``` /// ```
pub const INDENT: bool = false; #[settable]
#[default(false)]
pub indent: bool,
/// Content to fill the space between the title and the page number. Can be /// Content to fill the space between the title and the page number. Can be
/// set to `none` to disable filling. The default is `{repeat[.]}`. /// set to `none` to disable filling. The default is `{repeat[.]}`.
@ -72,12 +69,9 @@ impl OutlineNode {
/// ///
/// = A New Beginning /// = A New Beginning
/// ``` /// ```
#[property(referenced)] #[settable]
pub const FILL: Option<Content> = Some(RepeatNode(TextNode::packed(".")).pack()); #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))]
pub fill: Option<Content>,
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
Ok(Self.pack())
}
} }
impl Prepare for OutlineNode { impl Prepare for OutlineNode {
@ -91,7 +85,7 @@ impl Prepare for OutlineNode {
.locate(Selector::node::<HeadingNode>()) .locate(Selector::node::<HeadingNode>())
.into_iter() .into_iter()
.map(|(_, node)| node) .map(|(_, node)| node)
.filter(|node| node.field("outlined").unwrap() == Value::Bool(true)) .filter(|node| *node.field("outlined").unwrap() == Value::Bool(true))
.map(|node| Value::Content(node.clone())) .map(|node| Value::Content(node.clone()))
.collect(); .collect();
@ -107,7 +101,7 @@ impl Show for OutlineNode {
_: &Content, _: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let mut seq = vec![ParbreakNode.pack()]; let mut seq = vec![ParbreakNode::new().pack()];
if let Some(title) = styles.get(Self::TITLE) { if let Some(title) = styles.get(Self::TITLE) {
let body = title.clone().unwrap_or_else(|| { let body = title.clone().unwrap_or_else(|| {
TextNode::packed(match styles.get(TextNode::LANG) { TextNode::packed(match styles.get(TextNode::LANG) {
@ -117,7 +111,7 @@ impl Show for OutlineNode {
}); });
seq.push( seq.push(
HeadingNode { title: body, level: NonZeroUsize::new(1).unwrap() } HeadingNode::new(body)
.pack() .pack()
.styled(HeadingNode::NUMBERING, None) .styled(HeadingNode::NUMBERING, None)
.styled(HeadingNode::OUTLINED, false), .styled(HeadingNode::OUTLINED, false),
@ -129,26 +123,26 @@ impl Show for OutlineNode {
let mut ancestors: Vec<&Content> = vec![]; let mut ancestors: Vec<&Content> = vec![];
for (_, node) in vt.locate(Selector::node::<HeadingNode>()) { for (_, node) in vt.locate(Selector::node::<HeadingNode>()) {
if node.field("outlined").unwrap() != Value::Bool(true) { if *node.field("outlined").unwrap() != Value::Bool(true) {
continue; continue;
} }
let heading = node.to::<HeadingNode>().unwrap(); let heading = node.to::<HeadingNode>().unwrap();
if let Some(depth) = depth { if let Some(depth) = depth {
if depth < heading.level { if depth < heading.level() {
continue; continue;
} }
} }
while ancestors.last().map_or(false, |last| { while ancestors.last().map_or(false, |last| {
last.to::<HeadingNode>().unwrap().level >= heading.level last.to::<HeadingNode>().unwrap().level() >= heading.level()
}) { }) {
ancestors.pop(); ancestors.pop();
} }
// Adjust the link destination a bit to the topleft so that the // Adjust the link destination a bit to the topleft so that the
// heading is fully visible. // heading is fully visible.
let mut loc = node.field("loc").unwrap().cast::<Location>().unwrap(); let mut loc = node.field("loc").unwrap().clone().cast::<Location>().unwrap();
loc.pos -= Point::splat(Abs::pt(10.0)); loc.pos -= Point::splat(Abs::pt(10.0));
// Add hidden ancestors numberings to realize the indent. // Add hidden ancestors numberings to realize the indent.
@ -156,21 +150,21 @@ impl Show for OutlineNode {
let hidden: Vec<_> = ancestors let hidden: Vec<_> = ancestors
.iter() .iter()
.map(|node| node.field("numbers").unwrap()) .map(|node| node.field("numbers").unwrap())
.filter(|numbers| *numbers != Value::None) .filter(|&numbers| *numbers != Value::None)
.map(|numbers| numbers.display() + SpaceNode.pack()) .map(|numbers| numbers.clone().display() + SpaceNode::new().pack())
.collect(); .collect();
if !hidden.is_empty() { if !hidden.is_empty() {
seq.push(HideNode(Content::sequence(hidden)).pack()); seq.push(HideNode::new(Content::sequence(hidden)).pack());
seq.push(SpaceNode.pack()); seq.push(SpaceNode::new().pack());
} }
} }
// Format the numbering. // Format the numbering.
let mut start = heading.title.clone(); let mut start = heading.title();
let numbers = node.field("numbers").unwrap(); let numbers = node.field("numbers").unwrap();
if numbers != Value::None { if *numbers != Value::None {
start = numbers.display() + SpaceNode.pack() + start; start = numbers.clone().display() + SpaceNode::new().pack() + start;
}; };
// Add the numbering and section name. // Add the numbering and section name.
@ -178,30 +172,27 @@ impl Show for OutlineNode {
// Add filler symbols between the section name and page number. // Add filler symbols between the section name and page number.
if let Some(filler) = styles.get(Self::FILL) { if let Some(filler) = styles.get(Self::FILL) {
seq.push(SpaceNode.pack()); seq.push(SpaceNode::new().pack());
seq.push( seq.push(
BoxNode { BoxNode::new()
body: filler.clone(), .with_body(filler.clone())
width: Sizing::Fr(Fr::one()), .with_width(Fr::one().into())
height: Smart::Auto,
}
.pack(), .pack(),
); );
seq.push(SpaceNode.pack()); seq.push(SpaceNode::new().pack());
} else { } else {
let amount = Spacing::Fr(Fr::one()); seq.push(HNode::new(Fr::one().into()).pack());
seq.push(HNode { amount, weak: false }.pack());
} }
// Add the page number and linebreak. // Add the page number and linebreak.
let end = TextNode::packed(eco_format!("{}", loc.page)); let end = TextNode::packed(eco_format!("{}", loc.page));
seq.push(end.linked(Destination::Internal(loc))); seq.push(end.linked(Destination::Internal(loc)));
seq.push(LinebreakNode { justify: false }.pack()); seq.push(LinebreakNode::new().pack());
ancestors.push(node); ancestors.push(node);
} }
seq.push(ParbreakNode.pack()); seq.push(ParbreakNode::new().pack());
Ok(Content::sequence(seq)) Ok(Content::sequence(seq))
} }

View File

@ -1,7 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
/// # Reference
/// A reference to a label. /// A reference to a label.
/// ///
/// *Note: This function is currently unimplemented.* /// *Note: This function is currently unimplemented.*
@ -16,33 +15,18 @@ use crate::text::TextNode;
/// created by typing an `@` followed by the name of the label (e.g. `[= /// created by typing an `@` followed by the name of the label (e.g. `[=
/// Introduction <intro>]` can be referenced by typing `[@intro]`). /// Introduction <intro>]` can be referenced by typing `[@intro]`).
/// ///
/// ## Parameters /// Display: Reference
/// - target: `Label` (positional, required) /// Category: meta
/// The label that should be referenced. #[node(Show)]
/// pub struct RefNode {
/// ## Category /// The label that should be referenced.
/// meta #[positional]
#[func] #[required]
#[capable(Show)] pub target: EcoString,
#[derive(Debug, Hash)]
pub struct RefNode(pub EcoString);
#[node]
impl RefNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("target")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"target" => Some(Value::Str(self.0.clone().into())),
_ => None,
}
}
} }
impl Show for RefNode { impl Show for RefNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
Ok(TextNode::packed(eco_format!("@{}", self.0))) Ok(TextNode::packed(eco_format!("@{}", self.target())))
} }
} }

View File

@ -15,16 +15,16 @@ pub use typst::diag::{bail, error, At, SourceResult, StrResult};
pub use typst::doc::*; pub use typst::doc::*;
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::eval::{ pub use typst::eval::{
array, castable, dict, format_str, func, Args, Array, AutoValue, Cast, CastInfo, array, cast_from_value, cast_to_value, dict, format_str, func, Args, Array, Cast,
Dict, Func, NoneValue, Str, Symbol, Value, Vm, CastInfo, Dict, Func, Never, Str, Symbol, Value, Vm,
}; };
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::geom::*; pub use typst::geom::*;
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::model::{ pub use typst::model::{
capability, capable, node, Content, Finalize, Fold, Introspector, Label, Node, node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Prepare,
NodeId, Prepare, Resolve, Selector, Show, StabilityProvider, StyleChain, StyleMap, Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec,
StyleVec, Unlabellable, Vt, Unlabellable, Vt,
}; };
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::syntax::{Span, Spanned}; pub use typst::syntax::{Span, Spanned};

View File

@ -1,9 +1,8 @@
//! Node interaction. //! Node interaction.
use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder}; use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder};
/// How a node interacts with other nodes. /// How a node interacts with other nodes.
#[capability]
pub trait Behave { pub trait Behave {
/// The node's interaction behaviour. /// The node's interaction behaviour.
fn behaviour(&self) -> Behaviour; fn behaviour(&self) -> Behaviour;
@ -23,7 +22,7 @@ pub enum Behaviour {
/// after it. Furthermore, per consecutive run of weak nodes, only one /// after it. Furthermore, per consecutive run of weak nodes, only one
/// survives: The one with the lowest weakness level (or the larger one if /// survives: The one with the lowest weakness level (or the larger one if
/// there is a tie). /// there is a tie).
Weak(u8), Weak(usize),
/// A node that enables adjacent weak nodes to exist. The default. /// A node that enables adjacent weak nodes to exist. The default.
Supportive, Supportive,
/// A node that destroys adjacent weak nodes. /// A node that destroys adjacent weak nodes.

View File

@ -24,49 +24,43 @@ pub trait ContentExt {
/// Transform this content's contents without affecting layout. /// Transform this content's contents without affecting layout.
fn moved(self, delta: Axes<Rel<Length>>) -> Self; fn moved(self, delta: Axes<Rel<Length>>) -> Self;
/// Fill the frames resulting from a content.
fn filled(self, fill: Paint) -> Self;
/// Stroke the frames resulting from a content.
fn stroked(self, stroke: Stroke) -> Self;
} }
impl ContentExt for Content { impl ContentExt for Content {
fn strong(self) -> Self { fn strong(self) -> Self {
crate::text::StrongNode(self).pack() crate::text::StrongNode::new(self).pack()
} }
fn emph(self) -> Self { fn emph(self) -> Self {
crate::text::EmphNode(self).pack() crate::text::EmphNode::new(self).pack()
} }
fn underlined(self) -> Self { fn underlined(self) -> Self {
crate::text::UnderlineNode(self).pack() crate::text::UnderlineNode::new(self).pack()
} }
fn linked(self, dest: Destination) -> Self { fn linked(self, dest: Destination) -> Self {
self.styled(Meta::DATA, vec![Meta::Link(dest.clone())]) self.styled(MetaNode::DATA, vec![Meta::Link(dest.clone())])
} }
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self { fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
self.styled(crate::layout::AlignNode::ALIGNS, aligns) self.styled(crate::layout::AlignNode::ALIGNMENT, aligns)
} }
fn padded(self, padding: Sides<Rel<Length>>) -> Self { fn padded(self, padding: Sides<Rel<Length>>) -> Self {
crate::layout::PadNode { padding, body: self }.pack() crate::layout::PadNode::new(self)
.with_left(padding.left)
.with_top(padding.top)
.with_right(padding.right)
.with_bottom(padding.bottom)
.pack()
} }
fn moved(self, delta: Axes<Rel<Length>>) -> Self { fn moved(self, delta: Axes<Rel<Length>>) -> Self {
crate::layout::MoveNode { delta, body: self }.pack() crate::layout::MoveNode::new(self)
} .with_dx(delta.x)
.with_dy(delta.y)
fn filled(self, fill: Paint) -> Self { .pack()
FillNode { fill, child: self }.pack()
}
fn stroked(self, stroke: Stroke) -> Self {
StrokeNode { stroke, child: self }.pack()
} }
} }
@ -89,61 +83,3 @@ impl StyleMapExt for StyleMap {
); );
} }
} }
/// Fill the frames resulting from content.
#[capable(Layout)]
#[derive(Debug, Hash)]
struct FillNode {
/// How to fill the frames resulting from the `child`.
fill: Paint,
/// The content whose frames should be filled.
child: Content,
}
#[node]
impl FillNode {}
impl Layout for FillNode {
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut fragment = self.child.layout(vt, styles, regions)?;
for frame in &mut fragment {
let shape = Geometry::Rect(frame.size()).filled(self.fill);
frame.prepend(Point::zero(), Element::Shape(shape));
}
Ok(fragment)
}
}
/// Stroke the frames resulting from content.
#[capable(Layout)]
#[derive(Debug, Hash)]
struct StrokeNode {
/// How to stroke the frames resulting from the `child`.
stroke: Stroke,
/// The content whose frames should be stroked.
child: Content,
}
#[node]
impl StrokeNode {}
impl Layout for StrokeNode {
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut fragment = self.child.layout(vt, styles, regions)?;
for frame in &mut fragment {
let shape = Geometry::Rect(frame.size()).stroked(self.stroke);
frame.prepend(Point::zero(), Element::Shape(shape));
}
Ok(fragment)
}
}

View File

@ -4,7 +4,6 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use super::TextNode; use super::TextNode;
use crate::prelude::*; use crate::prelude::*;
/// # Underline
/// Underline text. /// Underline text.
/// ///
/// ## Example /// ## Example
@ -12,19 +11,15 @@ use crate::prelude::*;
/// This is #underline[important]. /// This is #underline[important].
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Underline
/// - body: `Content` (positional, required) /// Category: text
/// The content to underline. #[node(Show)]
/// pub struct UnderlineNode {
/// ## Category /// The content to underline.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct UnderlineNode(pub Content);
#[node]
impl UnderlineNode {
/// How to stroke the line. The text color and thickness are read from the /// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`. /// font tables if `{auto}`.
/// ///
@ -35,8 +30,12 @@ impl UnderlineNode {
/// [care], /// [care],
/// ) /// )
/// ``` /// ```
#[property(shorthand, resolve, fold)] #[settable]
pub const STROKE: Smart<PartialStroke> = Smart::Auto; #[shorthand]
#[resolve]
#[fold]
#[default]
pub stroke: Smart<PartialStroke>,
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `{auto}`. /// if `{auto}`.
@ -46,8 +45,10 @@ impl UnderlineNode {
/// The Tale Of A Faraway Line I /// The Tale Of A Faraway Line I
/// ] /// ]
/// ``` /// ```
#[property(resolve)] #[settable]
pub const OFFSET: Smart<Length> = Smart::Auto; #[resolve]
#[default]
pub offset: Smart<Length>,
/// Amount that the line will be longer or shorter than its associated text. /// Amount that the line will be longer or shorter than its associated text.
/// ///
@ -56,8 +57,10 @@ impl UnderlineNode {
/// underline(extent: 2pt)[Chapter 1] /// underline(extent: 2pt)[Chapter 1]
/// ) /// )
/// ``` /// ```
#[property(resolve)] #[settable]
pub const EXTENT: Length = Length::zero(); #[resolve]
#[default]
pub extent: Length,
/// Whether the line skips sections in which it would collide with the /// Whether the line skips sections in which it would collide with the
/// glyphs. /// glyphs.
@ -66,23 +69,14 @@ impl UnderlineNode {
/// This #underline(evade: true)[is great]. /// This #underline(evade: true)[is great].
/// This #underline(evade: false)[is less great]. /// This #underline(evade: false)[is less great].
/// ``` /// ```
pub const EVADE: bool = true; #[settable]
#[default(true)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub evade: bool,
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl Show for UnderlineNode { impl Show for UnderlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled( Ok(self.body().styled(
TextNode::DECO, TextNode::DECO,
Decoration { Decoration {
line: DecoLine::Underline, line: DecoLine::Underline,
@ -95,7 +89,6 @@ impl Show for UnderlineNode {
} }
} }
/// # Overline
/// Add a line over text. /// Add a line over text.
/// ///
/// ## Example /// ## Example
@ -103,19 +96,15 @@ impl Show for UnderlineNode {
/// #overline[A line over text.] /// #overline[A line over text.]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Overline
/// - body: `Content` (positional, required) /// Category: text
/// The content to add a line over. #[node(Show)]
/// pub struct OverlineNode {
/// ## Category /// The content to add a line over.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct OverlineNode(pub Content);
#[node]
impl OverlineNode {
/// How to stroke the line. The text color and thickness are read from the /// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`. /// font tables if `{auto}`.
/// ///
@ -127,8 +116,12 @@ impl OverlineNode {
/// [The Forest Theme], /// [The Forest Theme],
/// ) /// )
/// ``` /// ```
#[property(shorthand, resolve, fold)] #[settable]
pub const STROKE: Smart<PartialStroke> = Smart::Auto; #[shorthand]
#[resolve]
#[fold]
#[default]
pub stroke: Smart<PartialStroke>,
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `{auto}`. /// if `{auto}`.
@ -138,8 +131,10 @@ impl OverlineNode {
/// The Tale Of A Faraway Line II /// The Tale Of A Faraway Line II
/// ] /// ]
/// ``` /// ```
#[property(resolve)] #[settable]
pub const OFFSET: Smart<Length> = Smart::Auto; #[resolve]
#[default]
pub offset: Smart<Length>,
/// Amount that the line will be longer or shorter than its associated text. /// Amount that the line will be longer or shorter than its associated text.
/// ///
@ -148,8 +143,10 @@ impl OverlineNode {
/// #set underline(extent: 4pt) /// #set underline(extent: 4pt)
/// #overline(underline[Typography Today]) /// #overline(underline[Typography Today])
/// ``` /// ```
#[property(resolve)] #[settable]
pub const EXTENT: Length = Length::zero(); #[resolve]
#[default]
pub extent: Length,
/// Whether the line skips sections in which it would collide with the /// Whether the line skips sections in which it would collide with the
/// glyphs. /// glyphs.
@ -163,23 +160,14 @@ impl OverlineNode {
/// [Temple], /// [Temple],
/// ) /// )
/// ``` /// ```
pub const EVADE: bool = true; #[settable]
#[default(true)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub evade: bool,
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl Show for OverlineNode { impl Show for OverlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled( Ok(self.body().styled(
TextNode::DECO, TextNode::DECO,
Decoration { Decoration {
line: DecoLine::Overline, line: DecoLine::Overline,
@ -192,7 +180,6 @@ impl Show for OverlineNode {
} }
} }
/// # Strikethrough
/// Strike through text. /// Strike through text.
/// ///
/// ## Example /// ## Example
@ -200,19 +187,15 @@ impl Show for OverlineNode {
/// This is #strike[not] relevant. /// This is #strike[not] relevant.
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Strikethrough
/// - body: `Content` (positional, required) /// Category: text
/// The content to strike through. #[node(Show)]
/// pub struct StrikeNode {
/// ## Category /// The content to strike through.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct StrikeNode(pub Content);
#[node]
impl StrikeNode {
/// How to stroke the line. The text color and thickness are read from the /// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`. /// font tables if `{auto}`.
/// ///
@ -223,8 +206,12 @@ impl StrikeNode {
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \ /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
/// This is #strike(stroke: 10pt)[redacted]. /// This is #strike(stroke: 10pt)[redacted].
/// ``` /// ```
#[property(shorthand, resolve, fold)] #[settable]
pub const STROKE: Smart<PartialStroke> = Smart::Auto; #[shorthand]
#[resolve]
#[fold]
#[default]
pub stroke: Smart<PartialStroke>,
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `{auto}`. /// if `{auto}`.
@ -236,8 +223,10 @@ impl StrikeNode {
/// This is #strike(offset: auto)[low-ish]. \ /// This is #strike(offset: auto)[low-ish]. \
/// This is #strike(offset: -3.5pt)[on-top]. /// This is #strike(offset: -3.5pt)[on-top].
/// ``` /// ```
#[property(resolve)] #[settable]
pub const OFFSET: Smart<Length> = Smart::Auto; #[resolve]
#[default]
pub offset: Smart<Length>,
/// Amount that the line will be longer or shorter than its associated text. /// Amount that the line will be longer or shorter than its associated text.
/// ///
@ -245,24 +234,15 @@ impl StrikeNode {
/// This #strike(extent: -2pt)[skips] parts of the word. /// This #strike(extent: -2pt)[skips] parts of the word.
/// This #strike(extent: 2pt)[extends] beyond the word. /// This #strike(extent: 2pt)[extends] beyond the word.
/// ``` /// ```
#[property(resolve)] #[settable]
pub const EXTENT: Length = Length::zero(); #[resolve]
#[default]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub extent: Length,
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl Show for StrikeNode { impl Show for StrikeNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled( Ok(self.body().styled(
TextNode::DECO, TextNode::DECO,
Decoration { Decoration {
line: DecoLine::Strikethrough, line: DecoLine::Strikethrough,
@ -294,6 +274,10 @@ impl Fold for Decoration {
} }
} }
cast_from_value! {
Decoration: "decoration",
}
/// A kind of decorative line. /// A kind of decorative line.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum DecoLine { pub enum DecoLine {

View File

@ -1,24 +1,12 @@
use super::TextNode; use super::TextNode;
use crate::prelude::*; use crate::prelude::*;
/// # Space
/// A text space. /// A text space.
/// ///
/// ## Category /// Display: Space
/// text /// Category: text
#[func] #[node(Unlabellable, Behave)]
#[capable(Unlabellable, Behave)] pub struct SpaceNode {}
#[derive(Debug, Hash)]
pub struct SpaceNode;
#[node]
impl SpaceNode {
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
Ok(Self.pack())
}
}
impl Unlabellable for SpaceNode {}
impl Behave for SpaceNode { impl Behave for SpaceNode {
fn behaviour(&self) -> Behaviour { fn behaviour(&self) -> Behaviour {
@ -26,7 +14,8 @@ impl Behave for SpaceNode {
} }
} }
/// # Line Break impl Unlabellable for SpaceNode {}
/// Inserts a line break. /// Inserts a line break.
/// ///
/// Advances the paragraph to the next line. A single trailing line break at the /// Advances the paragraph to the next line. A single trailing line break at the
@ -45,46 +34,34 @@ impl Behave for SpaceNode {
/// a backslash followed by whitespace. This always creates an unjustified /// a backslash followed by whitespace. This always creates an unjustified
/// break. /// break.
/// ///
/// ## Parameters /// Display: Line Break
/// - justify: `bool` (named) /// Category: text
/// Whether to justify the line before the break. #[node(Behave)]
///
/// This is useful if you found a better line break opportunity in your
/// justified text than Typst did.
///
/// ```example
/// #set par(justify: true)
/// #let jb = linebreak(justify: true)
///
/// I have manually tuned the #jb
/// line breaks in this paragraph #jb
/// for an _interesting_ result. #jb
/// ```
///
/// ## Category
/// text
#[func]
#[capable(Behave)]
#[derive(Debug, Hash)]
pub struct LinebreakNode { pub struct LinebreakNode {
/// Whether to justify the line before the break.
///
/// This is useful if you found a better line break opportunity in your
/// justified text than Typst did.
///
/// ```example
/// #set par(justify: true)
/// #let jb = linebreak(justify: true)
///
/// I have manually tuned the #jb
/// line breaks in this paragraph #jb
/// for an _interesting_ result. #jb
/// ```
#[named]
#[default(false)]
pub justify: bool, pub justify: bool,
} }
#[node]
impl LinebreakNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let justify = args.named("justify")?.unwrap_or(false);
Ok(Self { justify }.pack())
}
}
impl Behave for LinebreakNode { impl Behave for LinebreakNode {
fn behaviour(&self) -> Behaviour { fn behaviour(&self) -> Behaviour {
Behaviour::Destructive Behaviour::Destructive
} }
} }
/// # Strong Emphasis
/// Strongly emphasizes content by increasing the font weight. /// Strongly emphasizes content by increasing the font weight.
/// ///
/// Increases the current font weight by a given `delta`. /// Increases the current font weight by a given `delta`.
@ -104,42 +81,29 @@ impl Behave for LinebreakNode {
/// word boundaries. To strongly emphasize part of a word, you have to use the /// word boundaries. To strongly emphasize part of a word, you have to use the
/// function. /// function.
/// ///
/// ## Parameters /// Display: Strong Emphasis
/// - body: `Content` (positional, required) /// Category: text
/// The content to strongly emphasize. #[node(Show)]
/// pub struct StrongNode {
/// ## Category /// The content to strongly emphasize.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct StrongNode(pub Content);
#[node]
impl StrongNode {
/// The delta to apply on the font weight. /// The delta to apply on the font weight.
/// ///
/// ```example /// ```example
/// #set strong(delta: 0) /// #set strong(delta: 0)
/// No *effect!* /// No *effect!*
/// ``` /// ```
pub const DELTA: i64 = 300; #[settable]
#[default(300)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub delta: i64,
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl Show for StrongNode { impl Show for StrongNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(TextNode::DELTA, Delta(styles.get(Self::DELTA)))) Ok(self.body().styled(TextNode::DELTA, Delta(styles.get(Self::DELTA))))
} }
} }
@ -147,11 +111,15 @@ impl Show for StrongNode {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Delta(pub i64); pub struct Delta(pub i64);
castable! { cast_from_value! {
Delta, Delta,
v: i64 => Self(v), v: i64 => Self(v),
} }
cast_to_value! {
v: Delta => v.0.into()
}
impl Fold for Delta { impl Fold for Delta {
type Output = i64; type Output = i64;
@ -160,7 +128,6 @@ impl Fold for Delta {
} }
} }
/// # Emphasis
/// Emphasizes content by setting it in italics. /// Emphasizes content by setting it in italics.
/// ///
/// - If the current [text style]($func/text.style) is `{"normal"}`, /// - If the current [text style]($func/text.style) is `{"normal"}`,
@ -185,34 +152,19 @@ impl Fold for Delta {
/// enclose it in underscores (`_`). Note that this only works at word /// enclose it in underscores (`_`). Note that this only works at word
/// boundaries. To emphasize part of a word, you have to use the function. /// boundaries. To emphasize part of a word, you have to use the function.
/// ///
/// ## Parameters /// Display: Emphasis
/// - body: `Content` (positional, required) /// Category: text
/// The content to emphasize. #[node(Show)]
/// pub struct EmphNode {
/// ## Category /// The content to emphasize.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct EmphNode(pub Content);
#[node]
impl EmphNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
} }
impl Show for EmphNode { impl Show for EmphNode {
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(TextNode::EMPH, Toggle)) Ok(self.body().styled(TextNode::EMPH, Toggle))
} }
} }
@ -220,6 +172,15 @@ impl Show for EmphNode {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Toggle; pub struct Toggle;
cast_from_value! {
Toggle,
_: Value => Self,
}
cast_to_value! {
_: Toggle => Value::None
}
impl Fold for Toggle { impl Fold for Toggle {
type Output = bool; type Output = bool;
@ -228,7 +189,6 @@ impl Fold for Toggle {
} }
} }
/// # Lowercase
/// Convert text or content to lowercase. /// Convert text or content to lowercase.
/// ///
/// ## Example /// ## Example
@ -242,14 +202,13 @@ impl Fold for Toggle {
/// - text: `ToCase` (positional, required) /// - text: `ToCase` (positional, required)
/// The text to convert to lowercase. /// The text to convert to lowercase.
/// ///
/// ## Category /// Display: Lowercase
/// text /// Category: text
#[func] #[func]
pub fn lower(args: &mut Args) -> SourceResult<Value> { pub fn lower(args: &mut Args) -> SourceResult<Value> {
case(Case::Lower, args) case(Case::Lower, args)
} }
/// # Uppercase
/// Convert text or content to uppercase. /// Convert text or content to uppercase.
/// ///
/// ## Example /// ## Example
@ -263,8 +222,8 @@ pub fn lower(args: &mut Args) -> SourceResult<Value> {
/// - text: `ToCase` (positional, required) /// - text: `ToCase` (positional, required)
/// The text to convert to uppercase. /// The text to convert to uppercase.
/// ///
/// ## Category /// Display: Uppercase
/// text /// Category: text
#[func] #[func]
pub fn upper(args: &mut Args) -> SourceResult<Value> { pub fn upper(args: &mut Args) -> SourceResult<Value> {
case(Case::Upper, args) case(Case::Upper, args)
@ -272,21 +231,22 @@ pub fn upper(args: &mut Args) -> SourceResult<Value> {
/// Change the case of text. /// Change the case of text.
fn case(case: Case, args: &mut Args) -> SourceResult<Value> { fn case(case: Case, args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect("string or content")?; Ok(match args.expect("string or content")? {
Ok(match v { ToCase::Str(v) => Value::Str(case.apply(&v).into()),
Value::Str(v) => Value::Str(case.apply(&v).into()), ToCase::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
v => bail!(span, "expected string or content, found {}", v.type_name()),
}) })
} }
/// A value whose case can be changed. /// A value whose case can be changed.
struct ToCase; enum ToCase {
Str(Str),
Content(Content),
}
castable! { cast_from_value! {
ToCase, ToCase,
_: Str => Self, v: Str => Self::Str(v),
_: Content => Self, v: Content => Self::Content(v),
} }
/// A case transformation on text. /// A case transformation on text.
@ -308,7 +268,19 @@ impl Case {
} }
} }
/// # Small Capitals cast_from_value! {
Case,
"lower" => Self::Lower,
"upper" => Self::Upper,
}
cast_to_value! {
v: Case => Value::from(match v {
Case::Lower => "lower",
Case::Upper => "upper",
})
}
/// Display text in small capitals. /// Display text in small capitals.
/// ///
/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts /// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
@ -336,15 +308,14 @@ impl Case {
/// - text: `Content` (positional, required) /// - text: `Content` (positional, required)
/// The text to display to small capitals. /// The text to display to small capitals.
/// ///
/// ## Category /// Display: Small Capitals
/// text /// Category: text
#[func] #[func]
pub fn smallcaps(args: &mut Args) -> SourceResult<Value> { pub fn smallcaps(args: &mut Args) -> SourceResult<Value> {
let body: Content = args.expect("content")?; let body: Content = args.expect("content")?;
Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true))) Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
} }
/// # Blind Text
/// Create blind text. /// Create blind text.
/// ///
/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given /// This function yields a Latin-like _Lorem Ipsum_ blind text with the given
@ -367,8 +338,8 @@ pub fn smallcaps(args: &mut Args) -> SourceResult<Value> {
/// ///
/// - returns: string /// - returns: string
/// ///
/// ## Category /// Display: Blind Text
/// text /// Category: text
#[func] #[func]
pub fn lorem(args: &mut Args) -> SourceResult<Value> { pub fn lorem(args: &mut Args) -> SourceResult<Value> {
let words: usize = args.expect("number of words")?; let words: usize = args.expect("number of words")?;

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ use typst::syntax::is_newline;
use crate::prelude::*; use crate::prelude::*;
/// # Smart Quote
/// A language-aware quote that reacts to its context. /// A language-aware quote that reacts to its context.
/// ///
/// Automatically turns into an appropriate opening or closing quote based on /// Automatically turns into an appropriate opening or closing quote based on
@ -23,21 +22,15 @@ use crate::prelude::*;
/// This function also has dedicated syntax: The normal quote characters /// This function also has dedicated syntax: The normal quote characters
/// (`'` and `"`). Typst automatically makes your quotes smart. /// (`'` and `"`). Typst automatically makes your quotes smart.
/// ///
/// ## Parameters /// Display: Smart Quote
/// - double: `bool` (named) /// Category: text
/// Whether this should be a double quote.
///
/// ## Category
/// text
#[func]
#[capable]
#[derive(Debug, Hash)]
pub struct SmartQuoteNode {
pub double: bool,
}
#[node] #[node]
impl SmartQuoteNode { pub struct SmartQuoteNode {
/// Whether this should be a double quote.
#[named]
#[default(true)]
pub double: bool,
/// Whether smart quotes are enabled. /// Whether smart quotes are enabled.
/// ///
/// To disable smartness for a single quote, you can also escape it with a /// To disable smartness for a single quote, you can also escape it with a
@ -48,19 +41,9 @@ impl SmartQuoteNode {
/// ///
/// These are "dumb" quotes. /// These are "dumb" quotes.
/// ``` /// ```
pub const ENABLED: bool = true; #[settable]
#[default(true)]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub enabled: bool,
let double = args.named("double")?.unwrap_or(true);
Ok(Self { double }.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"double" => Some(Value::Bool(self.double)),
_ => None,
}
}
} }
/// State machine for smart quote substitution. /// State machine for smart quote substitution.

View File

@ -9,7 +9,6 @@ use super::{
use crate::layout::BlockNode; use crate::layout::BlockNode;
use crate::prelude::*; use crate::prelude::*;
/// # Raw Text / Code
/// Raw text with optional syntax highlighting. /// Raw text with optional syntax highlighting.
/// ///
/// Displays the text verbatim and in a monospace font. This is typically used /// Displays the text verbatim and in a monospace font. This is typically used
@ -35,71 +34,64 @@ use crate::prelude::*;
/// ``` /// ```
/// ```` /// ````
/// ///
/// ## Parameters /// Display: Raw Text / Code
/// - text: `EcoString` (positional, required) /// Category: text
/// The raw text. #[node(Prepare, Show, Finalize)]
///
/// You can also use raw blocks creatively to create custom syntaxes for
/// your automations.
///
/// ````example
/// // Parse numbers in raw blocks with the
/// // `mydsl` tag and sum them up.
/// #show raw.where(lang: "mydsl"): it => {
/// let sum = 0
/// for part in it.text.split("+") {
/// sum += int(part.trim())
/// }
/// sum
/// }
///
/// ```mydsl
/// 1 + 2 + 3 + 4 + 5
/// ```
/// ````
///
/// - block: `bool` (named)
/// Whether the raw text is displayed as a separate block.
///
/// ````example
/// // Display inline code in a small box
/// // that retains the correct baseline.
/// #show raw.where(block: false): box.with(
/// fill: luma(240),
/// inset: (x: 3pt, y: 0pt),
/// outset: (y: 3pt),
/// radius: 2pt,
/// )
///
/// // Display block code in a larger block
/// // with more padding.
/// #show raw.where(block: true): block.with(
/// fill: luma(240),
/// inset: 10pt,
/// radius: 4pt,
/// )
///
/// With `rg`, you can search through your files quickly.
///
/// ```bash
/// rg "Hello World"
/// ```
/// ````
///
/// ## Category
/// text
#[func]
#[capable(Prepare, Show, Finalize)]
#[derive(Debug, Hash)]
pub struct RawNode { pub struct RawNode {
/// The raw text. /// The raw text.
///
/// You can also use raw blocks creatively to create custom syntaxes for
/// your automations.
///
/// ````example
/// // Parse numbers in raw blocks with the
/// // `mydsl` tag and sum them up.
/// #show raw.where(lang: "mydsl"): it => {
/// let sum = 0
/// for part in it.text.split("+") {
/// sum += int(part.trim())
/// }
/// sum
/// }
///
/// ```mydsl
/// 1 + 2 + 3 + 4 + 5
/// ```
/// ````
#[positional]
#[required]
pub text: EcoString, pub text: EcoString,
/// Whether the raw text is displayed as a separate block.
pub block: bool,
}
#[node] /// Whether the raw text is displayed as a separate block.
impl RawNode { ///
/// ````example
/// // Display inline code in a small box
/// // that retains the correct baseline.
/// #show raw.where(block: false): box.with(
/// fill: luma(240),
/// inset: (x: 3pt, y: 0pt),
/// outset: (y: 3pt),
/// radius: 2pt,
/// )
///
/// // Display block code in a larger block
/// // with more padding.
/// #show raw.where(block: true): block.with(
/// fill: luma(240),
/// inset: 10pt,
/// radius: 4pt,
/// )
///
/// With `rg`, you can search through your files quickly.
///
/// ```bash
/// rg "Hello World"
/// ```
/// ````
#[named]
#[default(false)]
pub block: bool,
/// The language to syntax-highlight in. /// The language to syntax-highlight in.
/// ///
/// Apart from typical language tags known from Markdown, this supports the /// Apart from typical language tags known from Markdown, this supports the
@ -111,24 +103,9 @@ impl RawNode {
/// This is *Typst!* /// This is *Typst!*
/// ``` /// ```
/// ```` /// ````
#[property(referenced)] #[settable]
pub const LANG: Option<EcoString> = None; #[default]
pub lang: Option<EcoString>,
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self {
text: args.expect("text")?,
block: args.named("block")?.unwrap_or(false),
}
.pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"text" => Some(Value::Str(self.text.clone().into())),
"block" => Some(Value::Bool(self.block)),
_ => None,
}
}
} }
impl Prepare for RawNode { impl Prepare for RawNode {
@ -138,19 +115,14 @@ impl Prepare for RawNode {
mut this: Content, mut this: Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
this.push_field( this.push_field("lang", styles.get(Self::LANG).clone());
"lang",
match styles.get(Self::LANG) {
Some(lang) => Value::Str(lang.clone().into()),
None => Value::None,
},
);
Ok(this) Ok(this)
} }
} }
impl Show for RawNode { impl Show for RawNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
let text = self.text();
let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase());
let foreground = THEME let foreground = THEME
.settings .settings
@ -161,8 +133,8 @@ impl Show for RawNode {
let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
let root = match lang.as_deref() { let root = match lang.as_deref() {
Some("typc") => syntax::parse_code(&self.text), Some("typc") => syntax::parse_code(&text),
_ => syntax::parse(&self.text), _ => syntax::parse(&text),
}; };
let mut seq = vec![]; let mut seq = vec![];
@ -172,7 +144,7 @@ impl Show for RawNode {
vec![], vec![],
&highlighter, &highlighter,
&mut |node, style| { &mut |node, style| {
seq.push(styled(&self.text[node.range()], foreground, style)); seq.push(styled(&text[node.range()], foreground, style));
}, },
); );
@ -182,9 +154,9 @@ impl Show for RawNode {
{ {
let mut seq = vec![]; let mut seq = vec![];
let mut highlighter = syntect::easy::HighlightLines::new(syntax, &THEME); let mut highlighter = syntect::easy::HighlightLines::new(syntax, &THEME);
for (i, line) in self.text.lines().enumerate() { for (i, line) in text.lines().enumerate() {
if i != 0 { if i != 0 {
seq.push(LinebreakNode { justify: false }.pack()); seq.push(LinebreakNode::new().pack());
} }
for (style, piece) in for (style, piece) in
@ -196,16 +168,11 @@ impl Show for RawNode {
Content::sequence(seq) Content::sequence(seq)
} else { } else {
TextNode::packed(self.text.clone()) TextNode::packed(text)
}; };
if self.block { if self.block() {
realized = BlockNode { realized = BlockNode::new().with_body(realized).pack();
body: realized,
width: Smart::Auto,
height: Smart::Auto,
}
.pack();
} }
Ok(realized) Ok(realized)

View File

@ -154,7 +154,7 @@ impl<'a> ShapedText<'a> {
for family in families(self.styles) { for family in families(self.styles) {
if let Some(font) = world if let Some(font) = world
.book() .book()
.select(family, self.variant) .select(family.as_str(), self.variant)
.and_then(|id| world.font(id)) .and_then(|id| world.font(id))
{ {
expand(&font); expand(&font);
@ -209,7 +209,7 @@ impl<'a> ShapedText<'a> {
let world = vt.world(); let world = vt.world();
let font = world let font = world
.book() .book()
.select(family, self.variant) .select(family.as_str(), self.variant)
.and_then(|id| world.font(id))?; .and_then(|id| world.font(id))?;
let ttf = font.ttf(); let ttf = font.ttf();
let glyph_id = ttf.glyph_index('-')?; let glyph_id = ttf.glyph_index('-')?;
@ -351,7 +351,7 @@ fn shape_segment<'a>(
ctx: &mut ShapingContext, ctx: &mut ShapingContext,
base: usize, base: usize,
text: &str, text: &str,
mut families: impl Iterator<Item = &'a str> + Clone, mut families: impl Iterator<Item = FontFamily> + Clone,
) { ) {
// Fonts dont have newlines and tabs. // Fonts dont have newlines and tabs.
if text.chars().all(|c| c == '\n' || c == '\t') { if text.chars().all(|c| c == '\n' || c == '\t') {
@ -362,7 +362,7 @@ fn shape_segment<'a>(
let world = ctx.vt.world(); let world = ctx.vt.world();
let book = world.book(); let book = world.book();
let mut selection = families.find_map(|family| { let mut selection = families.find_map(|family| {
book.select(family, ctx.variant) book.select(family.as_str(), ctx.variant)
.and_then(|id| world.font(id)) .and_then(|id| world.font(id))
.filter(|font| !ctx.used.contains(font)) .filter(|font| !ctx.used.contains(font))
}); });
@ -549,7 +549,7 @@ pub fn variant(styles: StyleChain) -> FontVariant {
} }
/// Resolve a prioritized iterator over the font families. /// Resolve a prioritized iterator over the font families.
pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { pub fn families(styles: StyleChain) -> impl Iterator<Item = FontFamily> + Clone {
const FALLBACKS: &[&str] = &[ const FALLBACKS: &[&str] = &[
"linux libertine", "linux libertine",
"twitter color emoji", "twitter color emoji",
@ -562,9 +562,8 @@ pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
styles styles
.get(TextNode::FAMILY) .get(TextNode::FAMILY)
.0 .0
.iter() .into_iter()
.map(|family| family.as_str()) .chain(tail.iter().copied().map(FontFamily::new))
.chain(tail.iter().copied())
} }
/// Collect the tags of the OpenType features to apply. /// Collect the tags of the OpenType features to apply.

View File

@ -3,7 +3,6 @@ use typst::model::SequenceNode;
use super::{variant, SpaceNode, TextNode, TextSize}; use super::{variant, SpaceNode, TextNode, TextSize};
use crate::prelude::*; use crate::prelude::*;
/// # Subscript
/// Set text in subscript. /// Set text in subscript.
/// ///
/// The text is rendered smaller and its baseline is lowered. /// The text is rendered smaller and its baseline is lowered.
@ -13,19 +12,15 @@ use crate::prelude::*;
/// Revenue#sub[yearly] /// Revenue#sub[yearly]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Subscript
/// - body: `Content` (positional, required) /// Category: text
/// The text to display in subscript. #[node(Show)]
/// pub struct SubNode {
/// ## Category /// The text to display in subscript.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct SubNode(pub Content);
#[node]
impl SubNode {
/// Whether to prefer the dedicated subscript characters of the font. /// Whether to prefer the dedicated subscript characters of the font.
/// ///
/// If this is enabled, Typst first tries to transform the text to subscript /// If this is enabled, Typst first tries to transform the text to subscript
@ -36,19 +31,23 @@ impl SubNode {
/// N#sub(typographic: true)[1] /// N#sub(typographic: true)[1]
/// N#sub(typographic: false)[1] /// N#sub(typographic: false)[1]
/// ``` /// ```
pub const TYPOGRAPHIC: bool = true; #[settable]
#[default(true)]
pub typographic: bool,
/// The baseline shift for synthetic subscripts. Does not apply if /// The baseline shift for synthetic subscripts. Does not apply if
/// `typographic` is true and the font has subscript codepoints for the /// `typographic` is true and the font has subscript codepoints for the
/// given `body`. /// given `body`.
pub const BASELINE: Length = Em::new(0.2).into(); #[settable]
#[default(Em::new(0.2).into())]
pub baseline: Length,
/// The font size for synthetic subscripts. Does not apply if /// The font size for synthetic subscripts. Does not apply if
/// `typographic` is true and the font has subscript codepoints for the /// `typographic` is true and the font has subscript codepoints for the
/// given `body`. /// given `body`.
pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); #[settable]
#[default(TextSize(Em::new(0.6).into()))]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub size: TextSize,
Ok(Self(args.expect("body")?).pack())
}
} }
impl Show for SubNode { impl Show for SubNode {
@ -58,9 +57,10 @@ impl Show for SubNode {
_: &Content, _: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let body = self.body();
let mut transformed = None; let mut transformed = None;
if styles.get(Self::TYPOGRAPHIC) { if styles.get(Self::TYPOGRAPHIC) {
if let Some(text) = search_text(&self.0, true) { if let Some(text) = search_text(&body, true) {
if is_shapable(vt, &text, styles) { if is_shapable(vt, &text, styles) {
transformed = Some(TextNode::packed(text)); transformed = Some(TextNode::packed(text));
} }
@ -71,12 +71,11 @@ impl Show for SubNode {
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
map.set(TextNode::SIZE, styles.get(Self::SIZE)); map.set(TextNode::SIZE, styles.get(Self::SIZE));
self.0.clone().styled_with_map(map) body.styled_with_map(map)
})) }))
} }
} }
/// # Superscript
/// Set text in superscript. /// Set text in superscript.
/// ///
/// The text is rendered smaller and its baseline is raised. /// The text is rendered smaller and its baseline is raised.
@ -86,19 +85,15 @@ impl Show for SubNode {
/// 1#super[st] try! /// 1#super[st] try!
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Superscript
/// - body: `Content` (positional, required) /// Category: text
/// The text to display in superscript. #[node(Show)]
/// pub struct SuperNode {
/// ## Category /// The text to display in superscript.
/// text #[positional]
#[func] #[required]
#[capable(Show)] pub body: Content,
#[derive(Debug, Hash)]
pub struct SuperNode(pub Content);
#[node]
impl SuperNode {
/// Whether to prefer the dedicated superscript characters of the font. /// Whether to prefer the dedicated superscript characters of the font.
/// ///
/// If this is enabled, Typst first tries to transform the text to /// If this is enabled, Typst first tries to transform the text to
@ -109,19 +104,23 @@ impl SuperNode {
/// N#super(typographic: true)[1] /// N#super(typographic: true)[1]
/// N#super(typographic: false)[1] /// N#super(typographic: false)[1]
/// ``` /// ```
pub const TYPOGRAPHIC: bool = true; #[settable]
#[default(true)]
pub typographic: bool,
/// The baseline shift for synthetic superscripts. Does not apply if /// The baseline shift for synthetic superscripts. Does not apply if
/// `typographic` is true and the font has superscript codepoints for the /// `typographic` is true and the font has superscript codepoints for the
/// given `body`. /// given `body`.
pub const BASELINE: Length = Em::new(-0.5).into(); #[settable]
#[default(Em::new(-0.5).into())]
pub baseline: Length,
/// The font size for synthetic superscripts. Does not apply if /// The font size for synthetic superscripts. Does not apply if
/// `typographic` is true and the font has superscript codepoints for the /// `typographic` is true and the font has superscript codepoints for the
/// given `body`. /// given `body`.
pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); #[settable]
#[default(TextSize(Em::new(0.6).into()))]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { pub size: TextSize,
Ok(Self(args.expect("body")?).pack())
}
} }
impl Show for SuperNode { impl Show for SuperNode {
@ -131,9 +130,10 @@ impl Show for SuperNode {
_: &Content, _: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let body = self.body();
let mut transformed = None; let mut transformed = None;
if styles.get(Self::TYPOGRAPHIC) { if styles.get(Self::TYPOGRAPHIC) {
if let Some(text) = search_text(&self.0, false) { if let Some(text) = search_text(&body, false) {
if is_shapable(vt, &text, styles) { if is_shapable(vt, &text, styles) {
transformed = Some(TextNode::packed(text)); transformed = Some(TextNode::packed(text));
} }
@ -144,7 +144,7 @@ impl Show for SuperNode {
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
map.set(TextNode::SIZE, styles.get(Self::SIZE)); map.set(TextNode::SIZE, styles.get(Self::SIZE));
self.0.clone().styled_with_map(map) body.styled_with_map(map)
})) }))
} }
} }
@ -154,12 +154,12 @@ impl Show for SuperNode {
fn search_text(content: &Content, sub: bool) -> Option<EcoString> { fn search_text(content: &Content, sub: bool) -> Option<EcoString> {
if content.is::<SpaceNode>() { if content.is::<SpaceNode>() {
Some(' '.into()) Some(' '.into())
} else if let Some(text) = content.to::<TextNode>() { } else if let Some(node) = content.to::<TextNode>() {
convert_script(&text.0, sub) convert_script(&node.text(), sub)
} else if let Some(seq) = content.to::<SequenceNode>() { } else if let Some(seq) = content.to::<SequenceNode>() {
let mut full = EcoString::new(); let mut full = EcoString::new();
for item in seq.0.iter() { for item in seq.children() {
match search_text(item, sub) { match search_text(&item, sub) {
Some(text) => full.push_str(&text), Some(text) => full.push_str(&text),
None => return None, None => return None,
} }

View File

@ -1,10 +1,10 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::prelude::*; use crate::prelude::*;
/// # Image
/// A raster or vector graphic. /// A raster or vector graphic.
/// ///
/// Supported formats are PNG, JPEG, GIF and SVG. /// Supported formats are PNG, JPEG, GIF and SVG.
@ -18,62 +18,52 @@ use crate::prelude::*;
/// ] /// ]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Image
/// - path: `EcoString` (positional, required) /// Category: visualize
/// Path to an image file. #[node(Construct, Layout)]
///
/// - width: `Rel<Length>` (named)
/// The width of the image.
///
/// - height: `Rel<Length>` (named)
/// The height of the image.
///
/// ## Category
/// visualize
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct ImageNode { pub struct ImageNode {
pub image: Image, /// Path to an image file.
#[positional]
#[required]
pub path: EcoString,
/// The width of the image.
#[named]
#[default]
pub width: Smart<Rel<Length>>, pub width: Smart<Rel<Length>>,
/// The height of the image.
#[named]
#[default]
pub height: Smart<Rel<Length>>, pub height: Smart<Rel<Length>>,
/// How the image should adjust itself to a given area.
#[settable]
#[default(ImageFit::Cover)]
pub fit: ImageFit,
} }
#[node] impl Construct for ImageNode {
impl ImageNode {
/// How the image should adjust itself to a given area.
pub const FIT: ImageFit = ImageFit::Cover;
fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content> {
let Spanned { v: path, span } = let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?; args.expect::<Spanned<EcoString>>("path to image file")?;
let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into();
let full = vm.locate(&path).at(span)?; let _ = load(vm.world(), &path).at(span)?;
let buffer = vm.world().file(&full).at(span)?; let width = args.named::<Smart<Rel<Length>>>("width")?.unwrap_or_default();
let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); let height = args.named::<Smart<Rel<Length>>>("height")?.unwrap_or_default();
let format = match ext.to_lowercase().as_str() { Ok(ImageNode::new(path).with_width(width).with_height(height).pack())
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => bail!(span, "unknown image format"),
};
let image = Image::new(buffer, format).at(span)?;
let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?.unwrap_or_default();
Ok(ImageNode { image, width, height }.pack())
} }
} }
impl Layout for ImageNode { impl Layout for ImageNode {
fn layout( fn layout(
&self, &self,
_: &mut Vt, vt: &mut Vt,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let sizing = Axes::new(self.width, self.height); let image = load(vt.world(), &self.path()).unwrap();
let sizing = Axes::new(self.width(), self.height());
let region = sizing let region = sizing
.zip(regions.base()) .zip(regions.base())
.map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))) .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)))
@ -83,8 +73,8 @@ impl Layout for ImageNode {
let region_ratio = region.x / region.y; let region_ratio = region.x / region.y;
// Find out whether the image is wider or taller than the target size. // Find out whether the image is wider or taller than the target size.
let pxw = self.image.width() as f64; let pxw = image.width() as f64;
let pxh = self.image.height() as f64; let pxh = image.height() as f64;
let px_ratio = pxw / pxh; let px_ratio = pxw / pxh;
let wide = px_ratio > region_ratio; let wide = px_ratio > region_ratio;
@ -116,7 +106,7 @@ impl Layout for ImageNode {
// the frame to the target size, center aligning the image in the // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::new(fitted); let mut frame = Frame::new(fitted);
frame.push(Point::zero(), Element::Image(self.image.clone(), fitted)); frame.push(Point::zero(), Element::Image(image, fitted));
frame.resize(target, Align::CENTER_HORIZON); frame.resize(target, Align::CENTER_HORIZON);
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
@ -142,7 +132,7 @@ pub enum ImageFit {
Stretch, Stretch,
} }
castable! { cast_from_value! {
ImageFit, ImageFit,
/// The image should completely cover the area. This is the default. /// The image should completely cover the area. This is the default.
"cover" => Self::Cover, "cover" => Self::Cover,
@ -152,3 +142,27 @@ castable! {
/// this means that the image will be distorted. /// this means that the image will be distorted.
"stretch" => Self::Stretch, "stretch" => Self::Stretch,
} }
cast_to_value! {
fit: ImageFit => Value::from(match fit {
ImageFit::Cover => "cover",
ImageFit::Contain => "contain",
ImageFit::Stretch => "stretch",
})
}
/// Load an image from a path.
#[comemo::memoize]
fn load(world: Tracked<dyn World>, full: &str) -> StrResult<Image> {
let full = Path::new(full);
let buffer = world.file(full)?;
let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default();
let format = match ext.to_lowercase().as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => return Err("unknown image format".into()),
};
Image::new(buffer, format)
}

View File

@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
/// # Line
/// A line from one point to another. /// A line from one point to another.
/// ///
/// ## Example /// ## Example
@ -26,20 +25,20 @@ use crate::prelude::*;
/// The angle at which the line points away from the origin. Mutually /// The angle at which the line points away from the origin. Mutually
/// exclusive with `end`. /// exclusive with `end`.
/// ///
/// ## Category /// Display: Line
/// visualize /// Category: visualize
#[func] #[node(Construct, Layout)]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct LineNode { pub struct LineNode {
/// Where the line starts. /// Where the line starts.
#[named]
#[default]
pub start: Axes<Rel<Length>>, pub start: Axes<Rel<Length>>,
/// The offset from `start` where the line ends.
pub delta: Axes<Rel<Length>>,
}
#[node] /// The offset from `start` where the line ends.
impl LineNode { #[named]
#[default]
pub delta: Axes<Rel<Length>>,
/// How to stroke the line. This can be: /// How to stroke the line. This can be:
/// ///
/// - A length specifying the stroke's thickness. The color is inherited, /// - A length specifying the stroke's thickness. The color is inherited,
@ -52,12 +51,16 @@ impl LineNode {
/// ```example /// ```example
/// #line(length: 100%, stroke: 2pt + red) /// #line(length: 100%, stroke: 2pt + red)
/// ``` /// ```
#[property(resolve, fold)] #[settable]
pub const STROKE: PartialStroke = PartialStroke::default(); #[resolve]
#[fold]
#[default]
pub stroke: PartialStroke,
}
impl Construct for LineNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let start = args.named("start")?.unwrap_or_default(); let start = args.named("start")?.unwrap_or_default();
let delta = match args.named::<Axes<Rel<Length>>>("end")? { let delta = match args.named::<Axes<Rel<Length>>>("end")? {
Some(end) => end.zip(start).map(|(to, from)| to - from), Some(end) => end.zip(start).map(|(to, from)| to - from),
None => { None => {
@ -71,8 +74,7 @@ impl LineNode {
Axes::new(x, y) Axes::new(x, y)
} }
}; };
Ok(Self::new().with_start(start).with_delta(delta).pack())
Ok(Self { start, delta }.pack())
} }
} }
@ -86,13 +88,13 @@ impl Layout for LineNode {
let stroke = styles.get(Self::STROKE).unwrap_or_default(); let stroke = styles.get(Self::STROKE).unwrap_or_default();
let origin = self let origin = self
.start .start()
.resolve(styles) .resolve(styles)
.zip(regions.base()) .zip(regions.base())
.map(|(l, b)| l.relative_to(b)); .map(|(l, b)| l.relative_to(b));
let delta = self let delta = self
.delta .delta()
.resolve(styles) .resolve(styles)
.zip(regions.base()) .zip(regions.base())
.map(|(l, b)| l.relative_to(b)); .map(|(l, b)| l.relative_to(b));

View File

@ -2,7 +2,6 @@ use std::f64::consts::SQRT_2;
use crate::prelude::*; use crate::prelude::*;
/// # Rectangle
/// A rectangle with optional content. /// A rectangle with optional content.
/// ///
/// ## Example /// ## Example
@ -17,32 +16,28 @@ use crate::prelude::*;
/// ] /// ]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Rectangle
/// - body: `Content` (positional) /// Category: visualize
/// The content to place into the rectangle. #[node(Layout)]
///
/// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`.
///
/// - width: `Rel<Length>` (named)
/// The rectangle's width, relative to its parent container.
///
/// - height: `Rel<Length>` (named)
/// The rectangle's height, relative to its parent container.
///
/// ## Category
/// visualize
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct RectNode { pub struct RectNode {
/// The content to place into the rectangle.
///
/// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[default]
pub body: Option<Content>, pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] /// The rectangle's width, relative to its parent container.
impl RectNode { #[named]
#[default]
pub width: Smart<Rel<Length>>,
/// The rectangle's height, relative to its parent container.
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// How to fill the rectangle. /// How to fill the rectangle.
/// ///
/// When setting a fill, the default stroke disappears. To create a /// When setting a fill, the default stroke disappears. To create a
@ -51,7 +46,9 @@ impl RectNode {
/// ```example /// ```example
/// #rect(fill: blue) /// #rect(fill: blue)
/// ``` /// ```
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// How to stroke the rectangle. This can be: /// How to stroke the rectangle. This can be:
/// ///
@ -85,8 +82,11 @@ impl RectNode {
/// rect(stroke: 2pt + red), /// rect(stroke: 2pt + red),
/// ) /// )
/// ``` /// ```
#[property(resolve, fold)] #[settable]
pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto; #[resolve]
#[fold]
#[default]
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
/// How much to round the rectangle's corners, relative to the minimum of /// How much to round the rectangle's corners, relative to the minimum of
/// the width and height divided by two. This can be: /// the width and height divided by two. This can be:
@ -122,8 +122,11 @@ impl RectNode {
/// ), /// ),
/// ) /// )
/// ``` /// ```
#[property(resolve, fold)] #[settable]
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the rectangle's content. /// How much to pad the rectangle's content.
/// ///
@ -135,20 +138,19 @@ impl RectNode {
/// ```example /// ```example
/// #rect(inset: 0pt)[Tight]) /// #rect(inset: 0pt)[Tight])
/// ``` /// ```
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); #[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. /// 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]($func/box.outset) for more details.
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let width = args.named("width")?.unwrap_or_default(); pub outset: Sides<Option<Rel<Length>>>,
let height = args.named("height")?.unwrap_or_default();
let body = args.eat()?;
Ok(Self { body, width, height }.pack())
}
} }
impl Layout for RectNode { impl Layout for RectNode {
@ -163,8 +165,8 @@ impl Layout for RectNode {
styles, styles,
regions, regions,
ShapeKind::Rect, ShapeKind::Rect,
&self.body, &self.body(),
Axes::new(self.width, self.height), Axes::new(self.width(), self.height()),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE), styles.get(Self::STROKE),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -174,7 +176,6 @@ impl Layout for RectNode {
} }
} }
/// # Square
/// A square with optional content. /// A square with optional content.
/// ///
/// ## Example /// ## Example
@ -189,69 +190,77 @@ impl Layout for RectNode {
/// ] /// ]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Square
/// - body: `Content` (positional) /// Category: visualize
/// The content to place into the square. The square expands to fit this #[node(Construct, Layout)]
/// content, keeping the 1-1 aspect ratio.
///
/// When this is omitted, the square takes on a default size of at most
/// `{30pt}`.
///
/// - size: `Length` (named)
/// The square's side length. This is mutually exclusive with `width` and
/// `height`.
///
/// - width: `Rel<Length>` (named)
/// The square's width. This is mutually exclusive with `size` and `height`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// width.
///
/// - height: `Rel<Length>` (named)
/// The square's height. This is mutually exclusive with `size` and `width`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// height.
///
/// ## Category
/// visualize
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct SquareNode { pub struct SquareNode {
/// The content to place into the square. The square expands to fit this
/// content, keeping the 1-1 aspect ratio.
///
/// When this is omitted, the square takes on a default size of at most
/// `{30pt}`.
#[positional]
#[default]
pub body: Option<Content>, pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] /// The square's width. This is mutually exclusive with `size` and `height`.
impl SquareNode { ///
/// In contrast to `size`, this can be relative to the parent container's
/// width.
#[named]
#[default]
pub width: Smart<Rel<Length>>,
/// The square's height. This is mutually exclusive with `size` and `width`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// height.
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// How to fill the square. See the /// How to fill the square. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// How to stroke the square. See the [rectangle's /// How to stroke the square. See the [rectangle's
/// documentation]($func/rect.stroke) for more details. /// documentation]($func/rect.stroke) for more details.
#[property(resolve, fold)] #[settable]
pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto; #[resolve]
#[fold]
#[default]
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
/// How much to round the square's corners. See the [rectangle's /// How much to round the square's corners. See the [rectangle's
/// documentation]($func/rect.radius) for more details. /// documentation]($func/rect.radius) for more details.
#[property(resolve, fold)] #[settable]
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the square's content. See the [rectangle's /// How much to pad the square's content. See the [rectangle's
/// documentation]($func/rect.inset) for more details. /// documentation]($func/rect.inset) for more details.
/// ///
/// The default value is `{5pt}`. /// The default value is `{5pt}`.
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); #[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 /// How much to expand the square's size without affecting the layout. See
/// the [rectangle's documentation]($func/rect.outset) for more details. /// the [rectangle's documentation]($func/rect.outset) for more details.
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub outset: Sides<Option<Rel<Length>>>,
}
impl Construct for SquareNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
let width = match size { let width = match size {
@ -264,8 +273,12 @@ impl SquareNode {
size => size, size => size,
} }
.unwrap_or_default(); .unwrap_or_default();
let body = args.eat()?; let body = args.eat::<Content>()?;
Ok(Self { body, width, height }.pack()) Ok(Self::new()
.with_body(body)
.with_width(width)
.with_height(height)
.pack())
} }
} }
@ -281,8 +294,8 @@ impl Layout for SquareNode {
styles, styles,
regions, regions,
ShapeKind::Square, ShapeKind::Square,
&self.body, &self.body(),
Axes::new(self.width, self.height), Axes::new(self.width(), self.height()),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE), styles.get(Self::STROKE),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -292,7 +305,6 @@ impl Layout for SquareNode {
} }
} }
/// # Ellipse
/// An ellipse with optional content. /// An ellipse with optional content.
/// ///
/// ## Example /// ## Example
@ -308,59 +320,59 @@ impl Layout for SquareNode {
/// ] /// ]
/// ``` /// ```
/// ///
/// ## Parameters /// Display: Ellipse
/// - body: `Content` (positional) /// Category: visualize
/// The content to place into the ellipse. #[node(Layout)]
///
/// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`.
///
/// - width: `Rel<Length>` (named)
/// The ellipse's width, relative to its parent container.
///
/// - height: `Rel<Length>` (named)
/// The ellipse's height, relative to its parent container.
///
/// ## Category
/// visualize
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct EllipseNode { pub struct EllipseNode {
/// The content to place into the ellipse.
///
/// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[default]
pub body: Option<Content>, pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] /// The ellipse's width, relative to its parent container.
impl EllipseNode { #[named]
#[default]
pub width: Smart<Rel<Length>>,
/// The ellipse's height, relative to its parent container.
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// How to fill the ellipse. See the /// How to fill the ellipse. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// How to stroke the ellipse. See the [rectangle's /// How to stroke the ellipse. See the [rectangle's
/// documentation]($func/rect.stroke) for more details. /// documentation]($func/rect.stroke) for more details.
#[property(resolve, fold)] #[settable]
pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto; #[resolve]
#[fold]
#[default]
pub stroke: Smart<Option<PartialStroke>>,
/// How much to pad the ellipse's content. See the [rectangle's /// How much to pad the ellipse's content. See the [rectangle's
/// documentation]($func/rect.inset) for more details. /// documentation]($func/rect.inset) for more details.
/// ///
/// The default value is `{5pt}`. /// The default value is `{5pt}`.
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); #[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 /// How much to expand the ellipse's size without affecting the layout. See
/// the [rectangle's documentation]($func/rect.outset) for more details. /// the [rectangle's documentation]($func/rect.outset) for more details.
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { #[default]
let width = args.named("width")?.unwrap_or_default(); pub outset: Sides<Option<Rel<Length>>>,
let height = args.named("height")?.unwrap_or_default();
let body = args.eat()?;
Ok(Self { body, width, height }.pack())
}
} }
impl Layout for EllipseNode { impl Layout for EllipseNode {
@ -375,8 +387,8 @@ impl Layout for EllipseNode {
styles, styles,
regions, regions,
ShapeKind::Ellipse, ShapeKind::Ellipse,
&self.body, &self.body(),
Axes::new(self.width, self.height), Axes::new(self.width(), self.height()),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::STROKE).map(Sides::splat),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -386,7 +398,6 @@ impl Layout for EllipseNode {
} }
} }
/// # Circle
/// A circle with optional content. /// A circle with optional content.
/// ///
/// ## Example /// ## Example
@ -423,40 +434,68 @@ impl Layout for EllipseNode {
/// In contrast to `size`, this can be relative to the parent container's /// In contrast to `size`, this can be relative to the parent container's
/// height. /// height.
/// ///
/// ## Category /// Display: Circle
/// visualize /// Category: visualize
#[func] #[node(Construct, Layout)]
#[capable(Layout)]
#[derive(Debug, Hash)]
pub struct CircleNode { pub struct CircleNode {
/// The content to place into the circle. The circle expands to fit this
/// content, keeping the 1-1 aspect ratio.
#[positional]
#[default]
pub body: Option<Content>, pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] /// The circle's width. This is mutually exclusive with `radius` and
impl CircleNode { /// `height`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// width.
#[named]
#[default]
pub width: Smart<Rel<Length>>,
/// The circle's height.This is mutually exclusive with `radius` and
/// `width`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// height.
#[named]
#[default]
pub height: Smart<Rel<Length>>,
/// How to fill the circle. See the /// How to fill the circle. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
pub const FILL: Option<Paint> = None; #[settable]
#[default]
pub fill: Option<Paint>,
/// How to stroke the circle. See the [rectangle's /// How to stroke the circle. See the [rectangle's
/// documentation]($func/rect.stroke) for more details. /// documentation]($func/rect.stroke) for more details.
#[property(resolve, fold)] #[settable]
pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto; #[resolve]
#[fold]
#[default(Smart::Auto)]
pub stroke: Smart<Option<PartialStroke>>,
/// How much to pad the circle's content. See the [rectangle's /// How much to pad the circle's content. See the [rectangle's
/// documentation]($func/rect.inset) for more details. /// documentation]($func/rect.inset) for more details.
/// ///
/// The default value is `{5pt}`. /// The default value is `{5pt}`.
#[property(resolve, fold)] #[settable]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); #[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 /// How much to expand the circle's size without affecting the layout. See
/// the [rectangle's documentation]($func/rect.outset) for more details. /// the [rectangle's documentation]($func/rect.outset) for more details.
#[property(resolve, fold)] #[settable]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); #[resolve]
#[fold]
#[default]
pub outset: Sides<Option<Rel<Length>>>,
}
impl Construct for CircleNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let size = args let size = args
.named::<Smart<Length>>("radius")? .named::<Smart<Length>>("radius")?
@ -471,8 +510,12 @@ impl CircleNode {
size => size, size => size,
} }
.unwrap_or_default(); .unwrap_or_default();
let body = args.eat()?; let body = args.eat::<Content>()?;
Ok(Self { body, width, height }.pack()) Ok(Self::new()
.with_body(body)
.with_width(width)
.with_height(height)
.pack())
} }
} }
@ -488,8 +531,8 @@ impl Layout for CircleNode {
styles, styles,
regions, regions,
ShapeKind::Circle, ShapeKind::Circle,
&self.body, &self.body(),
Axes::new(self.width, self.height), Axes::new(self.width(), self.height()),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::STROKE).map(Sides::splat),
styles.get(Self::INSET), styles.get(Self::INSET),

View File

@ -1,48 +0,0 @@
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::Token;
use super::*;
/// Expand the `#[capability]` macro.
pub fn capability(item: syn::ItemTrait) -> Result<TokenStream> {
let ident = &item.ident;
Ok(quote! {
#item
impl ::typst::model::Capability for dyn #ident {}
})
}
/// Expand the `#[capable(..)]` macro.
pub fn capable(attr: TokenStream, item: syn::Item) -> Result<TokenStream> {
let (ident, generics) = match &item {
syn::Item::Struct(s) => (&s.ident, &s.generics),
syn::Item::Enum(s) => (&s.ident, &s.generics),
_ => bail!(item, "only structs and enums are supported"),
};
let (params, args, clause) = generics.split_for_impl();
let checks = Punctuated::<Ident, Token![,]>::parse_terminated
.parse2(attr)?
.into_iter()
.map(|capability| {
quote! {
if id == ::std::any::TypeId::of::<dyn #capability>() {
return Some(unsafe {
::typst::util::fat::vtable(self as &dyn #capability)
});
}
}
});
Ok(quote! {
#item
unsafe impl #params ::typst::model::Capable for #ident #args #clause {
fn vtable(&self, id: ::std::any::TypeId) -> ::std::option::Option<*const ()> {
#(#checks)*
None
}
}
})
}

View File

@ -1,11 +1,7 @@
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;
use super::*; use super::*;
/// Expand the `castable!` macro. /// Expand the `cast_from_value!` macro.
pub fn castable(stream: TokenStream) -> Result<TokenStream> { pub fn cast_from_value(stream: TokenStream) -> Result<TokenStream> {
let castable: Castable = syn::parse2(stream)?; let castable: Castable = syn::parse2(stream)?;
let ty = &castable.ty; let ty = &castable.ty;
@ -41,6 +37,77 @@ pub fn castable(stream: TokenStream) -> Result<TokenStream> {
}) })
} }
/// Expand the `cast_to_value!` macro.
pub fn cast_to_value(stream: TokenStream) -> Result<TokenStream> {
let cast: Cast = syn::parse2(stream)?;
let Pattern::Ty(pat, ty) = &cast.pattern else {
bail!(callsite, "expected pattern");
};
let expr = &cast.expr;
Ok(quote! {
impl ::std::convert::From<#ty> for ::typst::eval::Value {
fn from(#pat: #ty) -> Self {
#expr
}
}
})
}
struct Castable {
ty: syn::Type,
name: Option<syn::LitStr>,
casts: Punctuated<Cast, Token![,]>,
}
struct Cast {
attrs: Vec<syn::Attribute>,
pattern: Pattern,
expr: syn::Expr,
}
enum Pattern {
Str(syn::LitStr),
Ty(syn::Pat, syn::Type),
}
impl Parse for Castable {
fn parse(input: ParseStream) -> Result<Self> {
let ty = input.parse()?;
let mut name = None;
if input.peek(Token![:]) {
let _: syn::Token![:] = input.parse()?;
name = Some(input.parse()?);
}
let _: syn::Token![,] = input.parse()?;
let casts = Punctuated::parse_terminated(input)?;
Ok(Self { ty, name, casts })
}
}
impl Parse for Cast {
fn parse(input: ParseStream) -> Result<Self> {
let attrs = input.call(syn::Attribute::parse_outer)?;
let pattern = input.parse()?;
let _: syn::Token![=>] = input.parse()?;
let expr = input.parse()?;
Ok(Self { attrs, pattern, expr })
}
}
impl Parse for Pattern {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(syn::LitStr) {
Ok(Pattern::Str(input.parse()?))
} else {
let pat = input.parse()?;
let _: syn::Token![:] = input.parse()?;
let ty = input.parse()?;
Ok(Pattern::Ty(pat, ty))
}
}
}
/// Create the castable's `is` function. /// Create the castable's `is` function.
fn create_is_func(castable: &Castable) -> TokenStream { fn create_is_func(castable: &Castable) -> TokenStream {
let mut string_arms = vec![]; let mut string_arms = vec![];
@ -163,7 +230,7 @@ fn create_describe_func(castable: &Castable) -> TokenStream {
if let Some(name) = &castable.name { if let Some(name) = &castable.name {
infos.push(quote! { infos.push(quote! {
CastInfo::Type(#name) ::typst::eval::CastInfo::Type(#name)
}); });
} }
@ -173,57 +240,3 @@ fn create_describe_func(castable: &Castable) -> TokenStream {
} }
} }
} }
struct Castable {
ty: syn::Type,
name: Option<syn::LitStr>,
casts: Punctuated<Cast, Token![,]>,
}
impl Parse for Castable {
fn parse(input: ParseStream) -> Result<Self> {
let ty = input.parse()?;
let mut name = None;
if input.peek(Token![:]) {
let _: syn::Token![:] = input.parse()?;
name = Some(input.parse()?);
}
let _: syn::Token![,] = input.parse()?;
let casts = Punctuated::parse_terminated(input)?;
Ok(Self { ty, name, casts })
}
}
struct Cast {
attrs: Vec<syn::Attribute>,
pattern: Pattern,
expr: syn::Expr,
}
impl Parse for Cast {
fn parse(input: ParseStream) -> Result<Self> {
let attrs = input.call(syn::Attribute::parse_outer)?;
let pattern = input.parse()?;
let _: syn::Token![=>] = input.parse()?;
let expr = input.parse()?;
Ok(Self { attrs, pattern, expr })
}
}
enum Pattern {
Str(syn::LitStr),
Ty(syn::Pat, syn::Type),
}
impl Parse for Pattern {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(syn::LitStr) {
Ok(Pattern::Str(input.parse()?))
} else {
let pat = input.parse()?;
let _: syn::Token![:] = input.parse()?;
let ty = input.parse()?;
Ok(Pattern::Ty(pat, ty))
}
}
}

View File

@ -1,30 +1,22 @@
use unscanny::Scanner;
use super::*; use super::*;
/// Expand the `#[func]` macro. /// Expand the `#[func]` macro.
pub fn func(item: syn::Item) -> Result<TokenStream> { pub fn func(item: syn::Item) -> Result<TokenStream> {
let docs = match &item { let mut docs = match &item {
syn::Item::Struct(item) => documentation(&item.attrs), syn::Item::Struct(item) => documentation(&item.attrs),
syn::Item::Enum(item) => documentation(&item.attrs), syn::Item::Enum(item) => documentation(&item.attrs),
syn::Item::Fn(item) => documentation(&item.attrs), syn::Item::Fn(item) => documentation(&item.attrs),
_ => String::new(), _ => String::new(),
}; };
let first = docs.lines().next().unwrap();
let display = first.strip_prefix("# ").unwrap();
let display = display.trim();
let mut docs = docs[first.len()..].to_string();
let (params, returns) = params(&mut docs)?; let (params, returns) = params(&mut docs)?;
let category = section(&mut docs, "Category", 2).expect("missing category");
let docs = docs.trim(); let docs = docs.trim();
let info = quote! { let info = quote! {
::typst::eval::FuncInfo { ::typst::eval::FuncInfo {
name, name,
display: #display, display: "TODO",
category: #category, category: "TODO",
docs: #docs, docs: #docs,
params: ::std::vec![#(#params),*], params: ::std::vec![#(#params),*],
returns: ::std::vec![#(#returns),*] returns: ::std::vec![#(#returns),*]
@ -82,7 +74,7 @@ pub fn func(item: syn::Item) -> Result<TokenStream> {
} }
/// Extract a section. /// Extract a section.
pub fn section(docs: &mut String, title: &str, level: usize) -> Option<String> { fn section(docs: &mut String, title: &str, level: usize) -> Option<String> {
let hashtags = "#".repeat(level); let hashtags = "#".repeat(level);
let needle = format!("\n{hashtags} {title}\n"); let needle = format!("\n{hashtags} {title}\n");
let start = docs.find(&needle)?; let start = docs.find(&needle)?;

View File

@ -2,32 +2,23 @@
extern crate proc_macro; extern crate proc_macro;
/// Return an error at the given item. #[macro_use]
macro_rules! bail { mod util;
(callsite, $fmt:literal $($tts:tt)*) => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(concat!("typst: ", $fmt) $($tts)*)
))
};
($item:expr, $fmt:literal $($tts:tt)*) => {
return Err(syn::Error::new_spanned(
&$item,
format!(concat!("typst: ", $fmt) $($tts)*)
))
};
}
mod capable;
mod castable; mod castable;
mod func; mod func;
mod node; mod node;
mod symbols; mod symbols;
use proc_macro::TokenStream as BoundaryStream; use proc_macro::TokenStream as BoundaryStream;
use proc_macro2::{TokenStream, TokenTree}; use proc_macro2::TokenStream;
use quote::{quote, quote_spanned}; use quote::quote;
use syn::{parse_quote, Ident, Result}; use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{parse_quote, Ident, Result, Token};
use unscanny::Scanner;
use self::util::*;
/// Implement `FuncType` for a type or function. /// Implement `FuncType` for a type or function.
#[proc_macro_attribute] #[proc_macro_attribute]
@ -38,33 +29,25 @@ pub fn func(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
/// Implement `Node` for a struct. /// Implement `Node` for a struct.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn node(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { pub fn node(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemImpl); let item = syn::parse_macro_input!(item as syn::ItemStruct);
node::node(item).unwrap_or_else(|err| err.to_compile_error()).into() node::node(stream.into(), item)
}
/// Implement `Capability` for a trait.
#[proc_macro_attribute]
pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemTrait);
capable::capability(item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implement `Capable` for a type.
#[proc_macro_attribute]
pub fn capable(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::Item);
capable::capable(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
/// Implement `Cast` and optionally `Type` for a type. /// Implement `Cast` and optionally `Type` for a type.
#[proc_macro] #[proc_macro]
pub fn castable(stream: BoundaryStream) -> BoundaryStream { pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
castable::castable(stream.into()) castable::cast_from_value(stream.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implement `From<T> for Value` for a type `T`.
#[proc_macro]
pub fn cast_to_value(stream: BoundaryStream) -> BoundaryStream {
castable::cast_to_value(stream.into())
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
@ -76,32 +59,3 @@ pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
/// Extract documentation comments from an attribute list.
fn documentation(attrs: &[syn::Attribute]) -> String {
let mut doc = String::new();
// Parse doc comments.
for attr in attrs {
if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
if meta.path.is_ident("doc") {
if let syn::Lit::Str(string) = &meta.lit {
let full = string.value();
let line = full.strip_prefix(' ').unwrap_or(&full);
doc.push_str(line);
doc.push('\n');
}
}
}
}
doc.trim().into()
}
/// Dedent documentation text.
fn dedent(text: &str) -> String {
text.lines()
.map(|s| s.strip_prefix(" ").unwrap_or(s))
.collect::<Vec<_>>()
.join("\n")
}

View File

@ -1,309 +1,370 @@
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::Token;
use super::*; use super::*;
/// Expand the `#[node]` macro. /// Expand the `#[node]` macro.
pub fn node(body: syn::ItemImpl) -> Result<TokenStream> { pub fn node(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
let node = prepare(body)?; let node = prepare(stream, &body)?;
create(&node) Ok(create(&node))
} }
/// Details about a node.
struct Node { struct Node {
body: syn::ItemImpl,
params: Punctuated<syn::GenericParam, Token![,]>,
self_ty: syn::Type,
self_name: String,
self_args: Punctuated<syn::GenericArgument, Token![,]>,
properties: Vec<Property>,
construct: Option<syn::ImplItemMethod>,
set: Option<syn::ImplItemMethod>,
field: Option<syn::ImplItemMethod>,
}
/// A style property.
struct Property {
attrs: Vec<syn::Attribute>, attrs: Vec<syn::Attribute>,
vis: syn::Visibility, vis: syn::Visibility,
name: Ident, ident: Ident,
value_ty: syn::Type, name: String,
output_ty: syn::Type, capable: Vec<Ident>,
default: syn::Expr, set: Option<syn::Block>,
skip: bool, fields: Vec<Field>,
referenced: bool, }
shorthand: Option<Shorthand>,
resolve: bool, struct Field {
fold: bool, attrs: Vec<syn::Attribute>,
vis: syn::Visibility,
ident: Ident,
with_ident: Ident,
name: String,
positional: bool,
required: bool,
variadic: bool,
named: bool,
shorthand: Option<Shorthand>,
settable: bool,
fold: bool,
resolve: bool,
skip: bool,
ty: syn::Type,
default: Option<syn::Expr>,
} }
/// The shorthand form of a style property.
enum Shorthand { enum Shorthand {
Positional, Positional,
Named(Ident), Named(Ident),
} }
/// Preprocess the impl block of a node. impl Node {
fn prepare(body: syn::ItemImpl) -> Result<Node> { fn inherent(&self) -> impl Iterator<Item = &Field> + Clone {
// Extract the generic type arguments. self.fields.iter().filter(|field| !field.settable)
let params = body.generics.params.clone();
// Extract the node type for which we want to generate properties.
let self_ty = (*body.self_ty).clone();
let self_path = match &self_ty {
syn::Type::Path(path) => path,
ty => bail!(ty, "must be a path type"),
};
// Split up the type into its name and its generic type arguments.
let last = self_path.path.segments.last().unwrap();
let self_name = last.ident.to_string();
let self_args = match &last.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.clone(),
_ => Punctuated::new(),
};
let mut properties = vec![];
let mut construct = None;
let mut set = None;
let mut field = None;
// Parse the properties and methods.
for item in &body.items {
match item {
syn::ImplItem::Const(item) => {
properties.push(prepare_property(item)?);
}
syn::ImplItem::Method(method) => {
match method.sig.ident.to_string().as_str() {
"construct" => construct = Some(method.clone()),
"set" => set = Some(method.clone()),
"field" => field = Some(method.clone()),
_ => bail!(method, "unexpected method"),
}
}
_ => bail!(item, "unexpected item"),
}
} }
Ok(Node { fn settable(&self) -> impl Iterator<Item = &Field> + Clone {
body, self.fields.iter().filter(|field| field.settable)
params, }
self_ty,
self_name,
self_args,
properties,
construct,
set,
field,
})
} }
/// Preprocess and validate a property constant. /// Preprocess the node's definition.
fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> { fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Node> {
let mut attrs = item.attrs.clone(); let syn::Fields::Named(named) = &body.fields else {
let tokens = match attrs bail!(body, "expected named fields");
.iter()
.position(|attr| attr.path.is_ident("property"))
.map(|i| attrs.remove(i))
{
Some(attr) => attr.parse_args::<TokenStream>()?,
None => TokenStream::default(),
}; };
let mut skip = false; let mut fields = vec![];
let mut shorthand = None; for field in &named.named {
let mut referenced = false; let Some(mut ident) = field.ident.clone() else {
let mut resolve = false; bail!(field, "expected named field");
let mut fold = false;
// Parse the `#[property(..)]` attribute.
let mut stream = tokens.into_iter().peekable();
while let Some(token) = stream.next() {
let ident = match token {
TokenTree::Ident(ident) => ident,
TokenTree::Punct(_) => continue,
_ => bail!(token, "invalid token"),
}; };
let mut arg = None; let mut attrs = field.attrs.clone();
if let Some(TokenTree::Group(group)) = stream.peek() { let settable = has_attr(&mut attrs, "settable");
let span = group.span(); if settable {
let string = group.to_string(); ident = Ident::new(&ident.to_string().to_uppercase(), ident.span());
let ident = string.trim_start_matches('(').trim_end_matches(')');
if !ident.chars().all(|c| c.is_ascii_alphabetic()) {
bail!(group, "invalid arguments");
} }
arg = Some(Ident::new(ident, span));
stream.next();
};
match ident.to_string().as_str() { let field = Field {
"skip" => skip = true, vis: field.vis.clone(),
"shorthand" => { ident: ident.clone(),
shorthand = Some(match arg { with_ident: Ident::new(&format!("with_{}", ident), ident.span()),
Some(name) => Shorthand::Named(name), name: kebab_case(&ident),
positional: has_attr(&mut attrs, "positional"),
required: has_attr(&mut attrs, "required"),
variadic: has_attr(&mut attrs, "variadic"),
named: has_attr(&mut attrs, "named"),
shorthand: parse_attr(&mut attrs, "shorthand")?.map(|v| match v {
None => Shorthand::Positional, None => Shorthand::Positional,
}); Some(ident) => Shorthand::Named(ident),
} }),
"referenced" => referenced = true,
"resolve" => resolve = true,
"fold" => fold = true,
_ => bail!(ident, "invalid attribute"),
}
}
if referenced && (fold || resolve) { settable,
bail!(item.ident, "referenced is mutually exclusive with fold and resolve"); fold: has_attr(&mut attrs, "fold"),
} resolve: has_attr(&mut attrs, "resolve"),
skip: has_attr(&mut attrs, "skip"),
// The type of the property's value is what the user of our macro wrote as ty: field.ty.clone(),
// type of the const, but the real type of the const will be a unique `Key` default: parse_attr(&mut attrs, "default")?.map(|opt| {
// type. opt.unwrap_or_else(|| parse_quote! { ::std::default::Default::default() })
let value_ty = item.ty.clone(); }),
let output_ty = if referenced {
parse_quote! { &'a #value_ty } attrs: {
} else if fold && resolve { validate_attrs(&attrs)?;
parse_quote! { attrs
<<#value_ty as ::typst::model::Resolve>::Output },
as ::typst::model::Fold>::Output
}
} else if fold {
parse_quote! { <#value_ty as ::typst::model::Fold>::Output }
} else if resolve {
parse_quote! { <#value_ty as ::typst::model::Resolve>::Output }
} else {
value_ty.clone()
}; };
Ok(Property { if !field.positional && !field.named && !field.variadic && !field.settable {
attrs, bail!(ident, "expected positional, named, variadic, or settable");
vis: item.vis.clone(), }
name: item.ident.clone(),
value_ty, if !field.required && !field.variadic && field.default.is_none() {
output_ty, bail!(ident, "non-required fields must have a default value");
default: item.expr.clone(), }
skip,
shorthand, fields.push(field);
referenced, }
resolve,
fold, let capable = Punctuated::<Ident, Token![,]>::parse_terminated
.parse2(stream)?
.into_iter()
.collect();
let mut attrs = body.attrs.clone();
Ok(Node {
vis: body.vis.clone(),
ident: body.ident.clone(),
name: body.ident.to_string().trim_end_matches("Node").to_lowercase(),
capable,
fields,
set: parse_attr(&mut attrs, "set")?.flatten(),
attrs: {
validate_attrs(&attrs)?;
attrs
},
}) })
} }
/// Produce the necessary items for a type to become a node. /// Produce the node's definition.
fn create(node: &Node) -> Result<TokenStream> { fn create(node: &Node) -> TokenStream {
let params = &node.params; let attrs = &node.attrs;
let self_ty = &node.self_ty; let vis = &node.vis;
let ident = &node.ident;
let name = &node.name;
let new = create_new_func(node);
let construct = node
.capable
.iter()
.all(|capability| capability != "Construct")
.then(|| create_construct_impl(node));
let set = create_set_impl(node);
let builders = node.inherent().map(create_builder_method);
let accessors = node.inherent().map(create_accessor_method);
let vtable = create_vtable(node);
let id_method = create_node_id_method(); let mut modules = vec![];
let name_method = create_node_name_method(node); let mut items = vec![];
let construct_func = create_node_construct_func(node); let scope = quote::format_ident!("__{}_keys", ident);
let set_func = create_node_set_func(node);
let properties_func = create_node_properties_func(node);
let field_method = create_node_field_method(node);
let node_impl = quote! { for field in node.settable() {
impl<#params> ::typst::model::Node for #self_ty { let ident = &field.ident;
#id_method let attrs = &field.attrs;
#name_method let vis = &field.vis;
#construct_func let ty = &field.ty;
#set_func modules.push(create_field_module(node, field));
#properties_func items.push(quote! {
#field_method
}
};
let mut modules: Vec<syn::ItemMod> = vec![];
let mut items: Vec<syn::ImplItem> = vec![];
let scope = quote::format_ident!("__{}_keys", node.self_name);
for property in node.properties.iter() {
let (key, module) = create_property_module(node, property);
modules.push(module);
let name = &property.name;
let attrs = &property.attrs;
let vis = &property.vis;
items.push(parse_quote! {
#(#attrs)* #(#attrs)*
#vis const #name: #scope::#name::#key #vis const #ident: #scope::#ident::Key<#ty>
= #scope::#name::Key(::std::marker::PhantomData); = #scope::#ident::Key(::std::marker::PhantomData);
}); });
} }
let mut body = node.body.clone(); quote! {
body.items = items; #(#attrs)*
#[::typst::eval::func]
#[derive(Debug, Clone, Hash)]
#[repr(transparent)]
#vis struct #ident(::typst::model::Content);
Ok(quote! { impl #ident {
#body #new
#(#builders)*
/// The node's span.
pub fn span(&self) -> Option<::typst::syntax::Span> {
self.0.span()
}
}
impl #ident {
#(#accessors)*
#(#items)*
}
impl ::typst::model::Node for #ident {
fn id() -> ::typst::model::NodeId {
static META: ::typst::model::NodeMeta = ::typst::model::NodeMeta {
name: #name,
vtable: #vtable,
};
::typst::model::NodeId::from_meta(&META)
}
fn pack(self) -> ::typst::model::Content {
self.0
}
}
#construct
#set
impl From<#ident> for ::typst::eval::Value {
fn from(value: #ident) -> Self {
value.0.into()
}
}
#[allow(non_snake_case)]
mod #scope { mod #scope {
use super::*; use super::*;
#node_impl
#(#modules)* #(#modules)*
} }
}) }
} }
/// Create the node's id method. /// Create the `new` function for the node.
fn create_node_id_method() -> syn::ImplItemMethod { fn create_new_func(node: &Node) -> TokenStream {
parse_quote! { let relevant = node.inherent().filter(|field| field.required || field.variadic);
fn id(&self) -> ::typst::model::NodeId { let params = relevant.clone().map(|field| {
::typst::model::NodeId::of::<Self>() let ident = &field.ident;
let ty = &field.ty;
quote! { #ident: #ty }
});
let pushes = relevant.map(|field| {
let ident = &field.ident;
let with_ident = &field.with_ident;
quote! { .#with_ident(#ident) }
});
let defaults = node
.inherent()
.filter_map(|field| field.default.as_ref().map(|default| (field, default)))
.map(|(field, default)| {
let with_ident = &field.with_ident;
quote! { .#with_ident(#default) }
});
quote! {
/// Create a new node.
pub fn new(#(#params),*) -> Self {
Self(::typst::model::Content::new::<Self>())
#(#pushes)*
#(#defaults)*
} }
} }
} }
/// Create the node's name method. /// Create a builder pattern method for a field.
fn create_node_name_method(node: &Node) -> syn::ImplItemMethod { fn create_builder_method(field: &Field) -> TokenStream {
let name = node.self_name.trim_end_matches("Node").to_lowercase(); let Field { with_ident, ident, name, ty, .. } = field;
parse_quote! { let doc = format!("Set the [`{}`](Self::{}) field.", name, ident);
fn name(&self) -> &'static str { quote! {
#name #[doc = #doc]
pub fn #with_ident(mut self, #ident: #ty) -> Self {
Self(self.0.with_field(#name, #ident))
} }
} }
} }
/// Create the node's `construct` function. /// Create an accessor methods for a field.
fn create_node_construct_func(node: &Node) -> syn::ImplItemMethod { fn create_accessor_method(field: &Field) -> TokenStream {
node.construct.clone().unwrap_or_else(|| { let Field { attrs, vis, ident, name, ty, .. } = field;
parse_quote! { quote! {
#(#attrs)*
#vis fn #ident(&self) -> #ty {
self.0.cast_field(#name)
}
}
}
/// Create the node's `Construct` implementation.
fn create_construct_impl(node: &Node) -> TokenStream {
let ident = &node.ident;
let shorthands = create_construct_shorthands(node);
let builders = node.inherent().map(create_construct_builder_call);
quote! {
impl ::typst::model::Construct for #ident {
fn construct( fn construct(
_: &::typst::eval::Vm, _: &::typst::eval::Vm,
args: &mut ::typst::eval::Args, args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Content> { ) -> ::typst::diag::SourceResult<::typst::model::Content> {
::typst::diag::bail!(args.span, "cannot be constructed manually"); #(#shorthands)*
Ok(::typst::model::Node::pack(
Self(::typst::model::Content::new::<Self>())
#(#builders)*))
} }
} }
}
}
/// Create let bindings for shorthands in the constructor.
fn create_construct_shorthands(node: &Node) -> impl Iterator<Item = TokenStream> + '_ {
let mut shorthands = vec![];
for field in node.inherent() {
if let Some(Shorthand::Named(named)) = &field.shorthand {
shorthands.push(named);
}
}
shorthands.sort();
shorthands.dedup_by_key(|ident| ident.to_string());
shorthands.into_iter().map(|ident| {
let string = ident.to_string();
quote! { let #ident = args.named(#string)?; }
}) })
} }
/// Create the node's `set` function. /// Create a builder call for the constructor.
fn create_node_set_func(node: &Node) -> syn::ImplItemMethod { fn create_construct_builder_call(field: &Field) -> TokenStream {
let user = node.set.as_ref().map(|method| { let name = &field.name;
let block = &method.block; let with_ident = &field.with_ident;
let mut value = if field.variadic {
quote! { args.all()? }
} else if field.required {
quote! { args.expect(#name)? }
} else if let Some(shorthand) = &field.shorthand {
match shorthand {
Shorthand::Positional => quote! { args.named_or_find(#name)? },
Shorthand::Named(named) => {
quote! { args.named(#name)?.or_else(|| #named.clone()) }
}
}
} else if field.named {
quote! { args.named(#name)? }
} else {
quote! { args.find()? }
};
if let Some(default) = &field.default {
value = quote! { #value.unwrap_or(#default) };
}
quote! { .#with_ident(#value) }
}
/// Create the node's `Set` implementation.
fn create_set_impl(node: &Node) -> TokenStream {
let ident = &node.ident;
let custom = node.set.as_ref().map(|block| {
quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; } quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; }
}); });
let mut shorthands = vec![]; let mut shorthands = vec![];
let sets: Vec<_> = node let sets: Vec<_> = node
.properties .settable()
.iter() .filter(|field| !field.skip)
.filter(|p| !p.skip) .map(|field| {
.map(|property| { let ident = &field.ident;
let name = &property.name; let name = &field.name;
let string = name.to_string().replace('_', "-").to_lowercase(); let value = match &field.shorthand {
let value = match &property.shorthand { Some(Shorthand::Positional) => quote! { args.named_or_find(#name)? },
Some(Shorthand::Positional) => quote! { args.named_or_find(#string)? },
Some(Shorthand::Named(named)) => { Some(Shorthand::Named(named)) => {
shorthands.push(named); shorthands.push(named);
quote! { args.named(#string)?.or_else(|| #named.clone()) } quote! { args.named(#name)?.or_else(|| #named.clone()) }
} }
None => quote! { args.named(#string)? }, None => quote! { args.named(#name)? },
}; };
quote! { styles.set_opt(Self::#name, #value); } quote! { styles.set_opt(Self::#ident, #value); }
}) })
.collect(); .collect();
@ -315,30 +376,12 @@ fn create_node_set_func(node: &Node) -> syn::ImplItemMethod {
quote! { let #ident = args.named(#string)?; } quote! { let #ident = args.named(#string)?; }
}); });
parse_quote! { let infos = node.fields.iter().filter(|p| !p.skip).map(|field| {
fn set( let name = &field.name;
args: &mut ::typst::eval::Args, let value_ty = &field.ty;
constructor: bool, let shorthand = matches!(field.shorthand, Some(Shorthand::Positional));
) -> ::typst::diag::SourceResult<::typst::model::StyleMap> { let docs = documentation(&field.attrs);
let mut styles = ::typst::model::StyleMap::new();
#user
#(#bindings)*
#(#sets)*
Ok(styles)
}
}
}
/// Create the node's `properties` function.
fn create_node_properties_func(node: &Node) -> syn::ImplItemMethod {
let infos = node.properties.iter().filter(|p| !p.skip).map(|property| {
let name = property.name.to_string().replace('_', "-").to_lowercase();
let value_ty = &property.value_ty;
let shorthand = matches!(property.shorthand, Some(Shorthand::Positional));
let docs = documentation(&property.attrs);
let docs = docs.trim(); let docs = docs.trim();
quote! { quote! {
::typst::eval::ParamInfo { ::typst::eval::ParamInfo {
name: #name, name: #name,
@ -355,167 +398,142 @@ fn create_node_properties_func(node: &Node) -> syn::ImplItemMethod {
} }
}); });
parse_quote! { quote! {
fn properties() -> ::std::vec::Vec<::typst::eval::ParamInfo> impl ::typst::model::Set for #ident {
where fn set(
Self: Sized args: &mut ::typst::eval::Args,
{ constructor: bool,
) -> ::typst::diag::SourceResult<::typst::model::StyleMap> {
let mut styles = ::typst::model::StyleMap::new();
#custom
#(#bindings)*
#(#sets)*
Ok(styles)
}
fn properties() -> ::std::vec::Vec<::typst::eval::ParamInfo> {
::std::vec![#(#infos),*] ::std::vec![#(#infos),*]
} }
} }
}
} }
/// Create the node's `field` method. /// Create the module for a single field.
fn create_node_field_method(node: &Node) -> syn::ImplItemMethod { fn create_field_module(node: &Node, field: &Field) -> TokenStream {
node.field.clone().unwrap_or_else(|| { let node_ident = &node.ident;
parse_quote! { let ident = &field.ident;
fn field( let name = &field.name;
&self, let ty = &field.ty;
_: &str, let default = &field.default;
) -> ::std::option::Option<::typst::eval::Value> {
None let mut output = quote! { #ty };
if field.resolve {
output = quote! { <#output as ::typst::model::Resolve>::Output };
} }
if field.fold {
output = quote! { <#output as ::typst::model::Fold>::Output };
} }
let value = if field.resolve && field.fold {
quote! {
values
.next()
.map(|value| {
::typst::model::Fold::fold(
::typst::model::Resolve::resolve(value, chain),
Self::get(chain, values),
)
}) })
} .unwrap_or(#default)
/// Process a single const item.
fn create_property_module(node: &Node, property: &Property) -> (syn::Type, syn::ItemMod) {
let params = &node.params;
let self_args = &node.self_args;
let name = &property.name;
let value_ty = &property.value_ty;
let output_ty = &property.output_ty;
let key = parse_quote! { Key<#value_ty, #self_args> };
let phantom_args = self_args.iter().filter(|arg| match arg {
syn::GenericArgument::Type(syn::Type::Path(path)) => {
node.params.iter().all(|param| match param {
syn::GenericParam::Const(c) => !path.path.is_ident(&c.ident),
_ => true,
})
} }
_ => true, } else if field.resolve {
}); quote! {
::typst::model::Resolve::resolve(
let name_const = create_property_name_const(node, property); values.next().unwrap_or(#default),
let node_func = create_property_node_func(node); chain
let get_method = create_property_get_method(property); )
let copy_assertion = create_property_copy_assertion(property); }
} else if field.fold {
quote! {
values
.next()
.map(|value| {
::typst::model::Fold::fold(
value,
Self::get(chain, values),
)
})
.unwrap_or(#default)
}
} else {
quote! {
values.next().unwrap_or(#default)
}
};
// Generate the contents of the module. // Generate the contents of the module.
let scope = quote! { let scope = quote! {
use super::*; use super::*;
pub struct Key<__T, #params>( pub struct Key<T>(pub ::std::marker::PhantomData<T>);
pub ::std::marker::PhantomData<(__T, #(#phantom_args,)*)> impl ::std::marker::Copy for Key<#ty> {}
); impl ::std::clone::Clone for Key<#ty> {
impl<#params> ::std::marker::Copy for #key {}
impl<#params> ::std::clone::Clone for #key {
fn clone(&self) -> Self { *self } fn clone(&self) -> Self { *self }
} }
impl<#params> ::typst::model::Key for #key { impl ::typst::model::Key for Key<#ty> {
type Value = #value_ty; type Value = #ty;
type Output<'a> = #output_ty; type Output = #output;
#name_const
#node_func fn id() -> ::typst::model::KeyId {
#get_method static META: ::typst::model::KeyMeta = ::typst::model::KeyMeta {
name: #name,
};
::typst::model::KeyId::from_meta(&META)
} }
#copy_assertion
};
// Generate the module code.
let module = parse_quote! {
#[allow(non_snake_case)]
pub mod #name { #scope }
};
(key, module)
}
/// Create the property's node method.
fn create_property_name_const(node: &Node, property: &Property) -> syn::ImplItemConst {
// The display name, e.g. `TextNode::BOLD`.
let name = format!("{}::{}", node.self_name, &property.name);
parse_quote! {
const NAME: &'static str = #name;
}
}
/// Create the property's node method.
fn create_property_node_func(node: &Node) -> syn::ImplItemMethod {
let self_ty = &node.self_ty;
parse_quote! {
fn node() -> ::typst::model::NodeId { fn node() -> ::typst::model::NodeId {
::typst::model::NodeId::of::<#self_ty>() ::typst::model::NodeId::of::<#node_ident>()
} }
}
}
/// Create the property's get method. fn get(
fn create_property_get_method(property: &Property) -> syn::ImplItemMethod { chain: ::typst::model::StyleChain,
let default = &property.default; mut values: impl ::std::iter::Iterator<Item = Self::Value>,
let value_ty = &property.value_ty; ) -> Self::Output {
let value = if property.referenced {
quote! {
values.next().unwrap_or_else(|| {
static LAZY: ::typst::model::once_cell::sync::Lazy<#value_ty>
= ::typst::model::once_cell::sync::Lazy::new(|| #default);
&*LAZY
})
}
} else if property.resolve && property.fold {
quote! {
match values.next().cloned() {
Some(value) => ::typst::model::Fold::fold(
::typst::model::Resolve::resolve(value, chain),
Self::get(chain, values),
),
None => #default,
}
}
} else if property.resolve {
quote! {
let value = values.next().cloned().unwrap_or_else(|| #default);
::typst::model::Resolve::resolve(value, chain)
}
} else if property.fold {
quote! {
match values.next().cloned() {
Some(value) => ::typst::model::Fold::fold(value, Self::get(chain, values)),
None => #default,
}
}
} else {
quote! {
values.next().copied().unwrap_or(#default)
}
};
parse_quote! {
fn get<'a>(
chain: ::typst::model::StyleChain<'a>,
mut values: impl ::std::iter::Iterator<Item = &'a Self::Value>,
) -> Self::Output<'a> {
#value #value
} }
} }
};
// Generate the module code.
quote! {
pub mod #ident { #scope }
}
} }
/// Create the assertion if the property's value must be copyable. /// Create the node's metadata vtable.
fn create_property_copy_assertion(property: &Property) -> Option<TokenStream> { fn create_vtable(node: &Node) -> TokenStream {
let value_ty = &property.value_ty; let ident = &node.ident;
let must_be_copy = !property.fold && !property.resolve && !property.referenced; let checks =
must_be_copy.then(|| { node.capable
quote_spanned! { value_ty.span() => .iter()
const _: fn() -> () = || { .filter(|&ident| ident != "Construct")
fn must_be_copy_fold_resolve_or_referenced<T: ::std::marker::Copy>() {} .map(|capability| {
must_be_copy_fold_resolve_or_referenced::<#value_ty>(); quote! {
}; if id == ::std::any::TypeId::of::<dyn #capability>() {
return Some(unsafe {
::typst::util::fat::vtable(&
Self(::typst::model::Content::new::<#ident>()) as &dyn #capability
)
});
}
}
});
quote! {
|id| {
#(#checks)*
None
}
} }
})
} }

View File

@ -1,23 +1,27 @@
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;
use super::*; use super::*;
/// Expand the `symbols!` macro. /// Expand the `symbols!` macro.
pub fn symbols(stream: TokenStream) -> Result<TokenStream> { pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
let list: List = syn::parse2(stream)?; let list: Punctuated<Symbol, Token![,]> =
let pairs = list.0.iter().map(Symbol::expand); Punctuated::parse_terminated.parse2(stream)?;
Ok(quote! { &[#(#pairs),*] }) let pairs = list.iter().map(|symbol| {
} let name = symbol.name.to_string();
let kind = match &symbol.kind {
struct List(Punctuated<Symbol, Token![,]>); Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), },
Kind::Multiple(variants) => {
impl Parse for List { let variants = variants.iter().map(|variant| {
fn parse(input: ParseStream) -> Result<Self> { let name = &variant.name;
Punctuated::parse_terminated(input).map(Self) let c = &variant.c;
quote! { (#name, #c) }
});
quote! {
typst::eval::Symbol::list(&[#(#variants),*])
} }
}
};
quote! { (#name, #kind) }
});
Ok(quote! { &[#(#pairs),*] })
} }
struct Symbol { struct Symbol {
@ -25,6 +29,16 @@ struct Symbol {
kind: Kind, kind: Kind,
} }
enum Kind {
Single(syn::LitChar),
Multiple(Punctuated<Variant, Token![,]>),
}
struct Variant {
name: String,
c: syn::LitChar,
}
impl Parse for Symbol { impl Parse for Symbol {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let name = input.call(Ident::parse_any)?; let name = input.call(Ident::parse_any)?;
@ -34,19 +48,6 @@ impl Parse for Symbol {
} }
} }
impl Symbol {
fn expand(&self) -> TokenStream {
let name = self.name.to_string();
let kind = self.kind.expand();
quote! { (#name, #kind) }
}
}
enum Kind {
Single(syn::LitChar),
Multiple(Punctuated<Variant, Token![,]>),
}
impl Parse for Kind { impl Parse for Kind {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
if input.peek(syn::LitChar) { if input.peek(syn::LitChar) {
@ -59,25 +60,6 @@ impl Parse for Kind {
} }
} }
impl Kind {
fn expand(&self) -> TokenStream {
match self {
Self::Single(c) => quote! { typst::eval::Symbol::new(#c), },
Self::Multiple(variants) => {
let variants = variants.iter().map(Variant::expand);
quote! {
typst::eval::Symbol::list(&[#(#variants),*])
}
}
}
}
}
struct Variant {
name: String,
c: syn::LitChar,
}
impl Parse for Variant { impl Parse for Variant {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let mut name = String::new(); let mut name = String::new();
@ -94,11 +76,3 @@ impl Parse for Variant {
Ok(Self { name, c }) Ok(Self { name, c })
} }
} }
impl Variant {
fn expand(&self) -> TokenStream {
let name = &self.name;
let c = &self.c;
quote! { (#name, #c) }
}
}

88
macros/src/util.rs Normal file
View File

@ -0,0 +1,88 @@
use super::*;
/// Return an error at the given item.
macro_rules! bail {
(callsite, $fmt:literal $($tts:tt)*) => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(concat!("typst: ", $fmt) $($tts)*)
))
};
($item:expr, $fmt:literal $($tts:tt)*) => {
return Err(syn::Error::new_spanned(
&$item,
format!(concat!("typst: ", $fmt) $($tts)*)
))
};
}
/// Whether an attribute list has a specified attribute.
pub fn has_attr(attrs: &mut Vec<syn::Attribute>, target: &str) -> bool {
take_attr(attrs, target).is_some()
}
/// Whether an attribute list has a specified attribute.
pub fn parse_attr<T: Parse>(
attrs: &mut Vec<syn::Attribute>,
target: &str,
) -> Result<Option<Option<T>>> {
take_attr(attrs, target)
.map(|attr| (!attr.tokens.is_empty()).then(|| attr.parse_args()).transpose())
.transpose()
}
/// Whether an attribute list has a specified attribute.
pub fn take_attr(
attrs: &mut Vec<syn::Attribute>,
target: &str,
) -> Option<syn::Attribute> {
attrs
.iter()
.position(|attr| attr.path.is_ident(target))
.map(|i| attrs.remove(i))
}
/// Ensure that no unrecognized attributes remain.
pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
for attr in attrs {
if !attr.path.is_ident("doc") {
let ident = attr.path.get_ident().unwrap();
bail!(ident, "unrecognized attribute: {:?}", ident.to_string());
}
}
Ok(())
}
/// Convert an identifier to a kebab-case string.
pub fn kebab_case(name: &Ident) -> String {
name.to_string().to_lowercase().replace('_', "-")
}
/// Dedent documentation text.
pub fn dedent(text: &str) -> String {
text.lines()
.map(|s| s.strip_prefix(" ").unwrap_or(s))
.collect::<Vec<_>>()
.join("\n")
}
/// Extract documentation comments from an attribute list.
pub fn documentation(attrs: &[syn::Attribute]) -> String {
let mut doc = String::new();
// Parse doc comments.
for attr in attrs {
if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
if meta.path.is_ident("doc") {
if let syn::Lit::Str(string) = &meta.lit {
let full = string.value();
let line = full.strip_prefix(' ').unwrap_or(&full);
doc.push_str(line);
doc.push('\n');
}
}
}
}
doc.trim().into()
}

View File

@ -7,14 +7,14 @@ use std::sync::Arc;
use ecow::EcoString; use ecow::EcoString;
use crate::eval::{dict, Dict, Value}; use crate::eval::{cast_from_value, cast_to_value, dict, Dict, Value};
use crate::font::Font; use crate::font::Font;
use crate::geom::{ use crate::geom::{
self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric, self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length,
Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
}; };
use crate::image::Image; use crate::image::Image;
use crate::model::{capable, node, Content, Fold, StableId, StyleChain}; use crate::model::{node, Content, Fold, StableId, StyleChain};
/// A finished document with metadata and page frames. /// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone, Hash)] #[derive(Debug, Default, Clone, Hash)]
@ -274,7 +274,7 @@ impl Frame {
if self.is_empty() { if self.is_empty() {
return; return;
} }
for meta in styles.get(Meta::DATA) { for meta in styles.get(MetaNode::DATA) {
if matches!(meta, Meta::Hidden) { if matches!(meta, Meta::Hidden) {
self.clear(); self.clear();
break; break;
@ -283,6 +283,14 @@ impl Frame {
} }
} }
/// Add a background fill.
pub fn fill(&mut self, fill: Paint) {
self.prepend(
Point::zero(),
Element::Shape(Geometry::Rect(self.size()).filled(fill)),
);
}
/// Add a fill and stroke with optional radius and outset to the frame. /// Add a fill and stroke with optional radius and outset to the frame.
pub fn fill_and_stroke( pub fn fill_and_stroke(
&mut self, &mut self,
@ -533,6 +541,15 @@ impl FromStr for Lang {
} }
} }
cast_from_value! {
Lang,
string: EcoString => Self::from_str(&string)?,
}
cast_to_value! {
v: Lang => v.as_str().into()
}
/// An identifier for a region somewhere in the world. /// An identifier for a region somewhere in the world.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Region([u8; 2]); pub struct Region([u8; 2]);
@ -559,8 +576,16 @@ impl FromStr for Region {
} }
} }
cast_from_value! {
Region,
string: EcoString => Self::from_str(&string)?,
}
cast_to_value! {
v: Region => v.as_str().into()
}
/// Meta information that isn't visible or renderable. /// Meta information that isn't visible or renderable.
#[capable]
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum Meta { pub enum Meta {
/// An internal or external link. /// An internal or external link.
@ -572,12 +597,16 @@ pub enum Meta {
Hidden, Hidden,
} }
/// Host for metadata.
#[node] #[node]
impl Meta { pub struct MetaNode {
/// Metadata that should be attached to all elements affected by this style /// Metadata that should be attached to all elements affected by this style
/// property. /// property.
#[property(fold, skip)] #[settable]
pub const DATA: Vec<Meta> = vec![]; #[fold]
#[skip]
#[default]
pub data: Vec<Meta>,
} }
impl Fold for Vec<Meta> { impl Fold for Vec<Meta> {
@ -589,6 +618,16 @@ impl Fold for Vec<Meta> {
} }
} }
cast_from_value! {
Meta: "meta",
}
impl PartialEq for Meta {
fn eq(&self, other: &Self) -> bool {
crate::util::hash128(self) == crate::util::hash128(other)
}
}
/// A link destination. /// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination { pub enum Destination {
@ -598,6 +637,19 @@ pub enum Destination {
Url(EcoString), Url(EcoString),
} }
cast_from_value! {
Destination,
loc: Location => Self::Internal(loc),
string: EcoString => Self::Url(string),
}
cast_to_value! {
v: Destination => match v {
Destination::Internal(loc) => loc.into(),
Destination::Url(url) => url.into(),
}
}
/// A physical location in a document. /// A physical location in a document.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location { pub struct Location {
@ -607,53 +659,21 @@ pub struct Location {
pub pos: Point, pub pos: Point,
} }
impl Location { cast_from_value! {
/// Encode into a user-facing dictionary. Location,
pub fn encode(&self) -> Dict { mut dict: Dict => {
dict! { let page = dict.take("page")?.cast()?;
"page" => Value::Int(self.page.get() as i64), let x: Length = dict.take("x")?.cast()?;
"x" => Value::Length(self.pos.x.into()), let y: Length = dict.take("y")?.cast()?;
"y" => Value::Length(self.pos.y.into()), dict.finish(&["page", "x", "y"])?;
} Self { page, pos: Point::new(x.abs, y.abs) }
} },
} }
/// Standard semantic roles. cast_to_value! {
#[derive(Debug, Copy, Clone, Eq, PartialEq)] v: Location => Value::Dict(dict! {
pub enum Role { "page" => Value::Int(v.page.get() as i64),
/// A paragraph. "x" => Value::Length(v.pos.x.into()),
Paragraph, "y" => Value::Length(v.pos.y.into()),
/// A heading of the given level and whether it should be part of the })
/// outline.
Heading { level: NonZeroUsize, outlined: bool },
/// A generic block-level subdivision.
GenericBlock,
/// A generic inline subdivision.
GenericInline,
/// A list and whether it is ordered.
List { ordered: bool },
/// A list item. Must have a list parent.
ListItem,
/// The label of a list item. Must have a list item parent.
ListLabel,
/// The body of a list item. Must have a list item parent.
ListItemBody,
/// A mathematical formula.
Formula,
/// A table.
Table,
/// A table row. Must have a table parent.
TableRow,
/// A table cell. Must have a table row parent.
TableCell,
/// A code fragment.
Code,
/// A page header.
Header,
/// A page footer.
Footer,
/// A page background.
Background,
/// A page foreground.
Foreground,
} }

View File

@ -1,18 +1,12 @@
pub use typst_macros::{cast_from_value, cast_to_value};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::Add; use std::ops::Add;
use std::str::FromStr;
use ecow::EcoString; use ecow::EcoString;
use super::{castable, Array, Dict, Func, Regex, Str, Value}; use super::{Array, Str, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::doc::{Destination, Lang, Location, Region};
use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::geom::{
Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio,
Rel, Sides, Smart,
};
use crate::model::{Content, Label, Selector, Transform};
use crate::syntax::Spanned; use crate::syntax::Spanned;
/// Cast from a value to a specific type. /// Cast from a value to a specific type.
@ -32,6 +26,191 @@ pub trait Cast<V = Value>: Sized {
} }
} }
impl Cast for Value {
fn is(_: &Value) -> bool {
true
}
fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
fn describe() -> CastInfo {
CastInfo::Any
}
}
impl<T: Cast> Cast<Spanned<Value>> for T {
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
fn describe() -> CastInfo {
T::describe()
}
}
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
fn describe() -> CastInfo {
T::describe()
}
}
cast_to_value! {
v: u8 => Value::Int(v as i64)
}
cast_to_value! {
v: u16 => Value::Int(v as i64)
}
cast_from_value! {
u32,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
"number must be at least zero"
} else {
"number too large"
}
})?,
}
cast_to_value! {
v: u32 => Value::Int(v as i64)
}
cast_to_value! {
v: i32 => Value::Int(v as i64)
}
cast_from_value! {
usize,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
"number must be at least zero"
} else {
"number too large"
}
})?,
}
cast_to_value! {
v: usize => Value::Int(v as i64)
}
cast_from_value! {
NonZeroUsize,
int: i64 => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}
cast_to_value! {
v: NonZeroUsize => Value::Int(v.get() as i64)
}
cast_from_value! {
char,
string: Str => {
let mut chars = string.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => c,
_ => Err("expected exactly one character")?,
}
},
}
cast_to_value! {
v: char => Value::Str(v.into())
}
cast_to_value! {
v: &str => Value::Str(v.into())
}
cast_from_value! {
EcoString,
v: Str => v.into(),
}
cast_to_value! {
v: EcoString => Value::Str(v.into())
}
cast_from_value! {
String,
v: Str => v.into(),
}
cast_to_value! {
v: String => Value::Str(v.into())
}
impl<T: Cast> Cast for Option<T> {
fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::None => Ok(None),
v if T::is(&v) => Ok(Some(T::cast(v)?)),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("none")
}
}
impl<T: Into<Value>> From<Option<T>> for Value {
fn from(v: Option<T>) -> Self {
match v {
Some(v) => v.into(),
None => Value::None,
}
}
}
impl<T: Cast> Cast for Vec<T> {
fn is(value: &Value) -> bool {
Array::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
value.cast::<Array>()?.into_iter().map(Value::cast).collect()
}
fn describe() -> CastInfo {
<Array as Cast>::describe()
}
}
impl<T: Into<Value>> From<Vec<T>> for Value {
fn from(v: Vec<T>) -> Self {
Value::Array(v.into_iter().map(Into::into).collect())
}
}
/// Describes a possible value for a cast. /// Describes a possible value for a cast.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum CastInfo { pub enum CastInfo {
@ -114,400 +293,19 @@ impl Add for CastInfo {
} }
} }
impl Cast for Value { /// Castable from nothing.
pub enum Never {}
impl Cast for Never {
fn is(_: &Value) -> bool { fn is(_: &Value) -> bool {
true false
} }
fn cast(value: Value) -> StrResult<Self> { fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
fn describe() -> CastInfo {
CastInfo::Any
}
}
impl<T: Cast> Cast<Spanned<Value>> for T {
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
fn describe() -> CastInfo {
T::describe()
}
}
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
fn is(value: &Spanned<Value>) -> bool {
T::is(&value.v)
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
fn describe() -> CastInfo {
T::describe()
}
}
castable! {
Dir: "direction",
}
castable! {
GenAlign: "alignment",
}
castable! {
Regex: "regular expression",
}
castable! {
Selector: "selector",
text: EcoString => Self::text(&text),
label: Label => Self::Label(label),
func: Func => func.select(None)?,
regex: Regex => Self::Regex(regex),
}
castable! {
Axes<GenAlign>: "2d alignment",
}
castable! {
PartialStroke: "stroke",
thickness: Length => Self {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
},
color: Color => Self {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
},
}
castable! {
u32,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
"number must be at least zero"
} else {
"number too large"
}
})?,
}
castable! {
usize,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
"number must be at least zero"
} else {
"number too large"
}
})?,
}
castable! {
NonZeroUsize,
int: i64 => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}
castable! {
Paint,
color: Color => Self::Solid(color),
}
castable! {
char,
string: Str => {
let mut chars = string.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => c,
_ => Err("expected exactly one character")?,
}
},
}
castable! {
EcoString,
string: Str => string.into(),
}
castable! {
String,
string: Str => string.into(),
}
castable! {
Transform,
content: Content => Self::Content(content),
func: Func => {
if func.argc().map_or(false, |count| count != 1) {
Err("function must have exactly one parameter")?
}
Self::Func(func)
},
}
castable! {
Axes<Option<GenAlign>>,
align: GenAlign => {
let mut aligns = Axes::default();
aligns.set(align.axis(), Some(align));
aligns
},
aligns: Axes<GenAlign> => aligns.map(Some),
}
castable! {
Axes<Rel<Length>>,
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?,
}
},
}
castable! {
Location,
mut dict: Dict => {
let page = dict.take("page")?.cast()?;
let x: Length = dict.take("x")?.cast()?;
let y: Length = dict.take("y")?.cast()?;
dict.finish(&["page", "x", "y"])?;
Self { page, pos: Point::new(x.abs, y.abs) }
},
}
castable! {
Destination,
loc: Location => Self::Internal(loc),
string: EcoString => Self::Url(string),
}
castable! {
FontStyle,
/// The default, typically upright style.
"normal" => Self::Normal,
/// A cursive style with custom letterform.
"italic" => Self::Italic,
/// Just a slanted version of the normal style.
"oblique" => Self::Oblique,
}
castable! {
FontWeight,
v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
/// Thin weight (100).
"thin" => Self::THIN,
/// Extra light weight (200).
"extralight" => Self::EXTRALIGHT,
/// Light weight (300).
"light" => Self::LIGHT,
/// Regular weight (400).
"regular" => Self::REGULAR,
/// Medium weight (500).
"medium" => Self::MEDIUM,
/// Semibold weight (600).
"semibold" => Self::SEMIBOLD,
/// Bold weight (700).
"bold" => Self::BOLD,
/// Extrabold weight (800).
"extrabold" => Self::EXTRABOLD,
/// Black weight (900).
"black" => Self::BLACK,
}
castable! {
FontStretch,
v: Ratio => Self::from_ratio(v.get() as f32),
}
castable! {
Lang,
string: EcoString => Self::from_str(&string)?,
}
castable! {
Region,
string: EcoString => Self::from_str(&string)?,
}
/// Castable from [`Value::None`].
pub struct NoneValue;
impl Cast for NoneValue {
fn is(value: &Value) -> bool {
matches!(value, Value::None)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::None => Ok(Self),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
CastInfo::Type("none")
}
}
impl<T: Cast> Cast for Option<T> {
fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::None => Ok(None),
v if T::is(&v) => Ok(Some(T::cast(v)?)),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("none")
}
}
/// Castable from [`Value::Auto`].
pub struct AutoValue;
impl Cast for AutoValue {
fn is(value: &Value) -> bool {
matches!(value, Value::Auto)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
CastInfo::Type("auto")
}
}
impl<T: Cast> Cast for Smart<T> {
fn is(value: &Value) -> bool {
matches!(value, Value::Auto) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self::Auto),
v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("auto")
}
}
impl<T> Cast for Sides<Option<T>>
where
T: Cast + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let x = take("x")?.or(rest);
let y = take("y")?.or(rest);
let sides = Sides {
left: take("left")?.or(x),
top: take("top")?.or(y),
right: take("right")?.or(x),
bottom: take("bottom")?.or(y),
};
dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
Ok(sides)
} else if T::is(&value) {
Ok(Self::splat(Some(T::cast(value)?)))
} else {
<Self as Cast>::error(value) <Self as Cast>::error(value)
} }
}
fn describe() -> CastInfo { fn describe() -> CastInfo {
T::describe() + CastInfo::Type("dictionary") CastInfo::Union(vec![])
}
}
impl<T> Cast for Corners<Option<T>>
where
T: Cast + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let left = take("left")?.or(rest);
let top = take("top")?.or(rest);
let right = take("right")?.or(rest);
let bottom = take("bottom")?.or(rest);
let corners = Corners {
top_left: take("top-left")?.or(top).or(left),
top_right: take("top-right")?.or(top).or(right),
bottom_right: take("bottom-right")?.or(bottom).or(right),
bottom_left: take("bottom-left")?.or(bottom).or(left),
};
dict.finish(&[
"top-left",
"top-right",
"bottom-right",
"bottom-left",
"left",
"top",
"right",
"bottom",
"rest",
])?;
Ok(corners)
} else if T::is(&value) {
Ok(Self::splat(Some(T::cast(value)?)))
} else {
<Self as Cast>::error(value)
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("dictionary")
} }
} }

View File

@ -1,3 +1,5 @@
pub use typst_macros::func;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::sync::Arc; use std::sync::Arc;

View File

@ -44,7 +44,7 @@ pub struct LangItems {
/// The id of the text node. /// The id of the text node.
pub text_id: NodeId, pub text_id: NodeId,
/// Get the string if this is a text node. /// Get the string if this is a text node.
pub text_str: fn(&Content) -> Option<&str>, pub text_str: fn(&Content) -> Option<EcoString>,
/// A smart quote: `'` or `"`. /// A smart quote: `'` or `"`.
pub smart_quote: fn(double: bool) -> Content, pub smart_quote: fn(double: bool) -> Content,
/// A paragraph break. /// A paragraph break.

View File

@ -20,8 +20,6 @@ mod ops;
mod scope; mod scope;
mod symbol; mod symbol;
pub use typst_macros::{castable, func};
pub use self::args::*; pub use self::args::*;
pub use self::array::*; pub use self::array::*;
pub use self::cast::*; pub use self::cast::*;

View File

@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Deref};
use ecow::EcoString; use ecow::EcoString;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{castable, dict, Array, Dict, Value}; use super::{cast_from_value, dict, Array, Dict, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::GenAlign; use crate::geom::GenAlign;
@ -479,6 +479,10 @@ impl Hash for Regex {
} }
} }
cast_from_value! {
Regex: "regular expression",
}
/// A pattern which can be searched for in a string. /// A pattern which can be searched for in a string.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum StrPattern { pub enum StrPattern {
@ -488,7 +492,7 @@ pub enum StrPattern {
Regex(Regex), Regex(Regex),
} }
castable! { cast_from_value! {
StrPattern, StrPattern,
text: Str => Self::Str(text), text: Str => Self::Str(text),
regex: Regex => Self::Regex(regex), regex: Regex => Self::Regex(regex),
@ -504,7 +508,7 @@ pub enum StrSide {
End, End,
} }
castable! { cast_from_value! {
StrSide, StrSide,
align: GenAlign => match align { align: GenAlign => match align {
GenAlign::Start => Self::Start, GenAlign::Start => Self::Start,

View File

@ -4,15 +4,15 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use siphasher::sip128::{Hasher128, SipHasher}; use siphasher::sip128::{Hasher128, SipHasher};
use super::{ use super::{
format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Module, cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func,
Str, Symbol, Label, Module, Str, Symbol,
}; };
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel};
use crate::syntax::{ast, Span}; use crate::syntax::{ast, Span};
/// A computational value. /// A computational value.
@ -122,6 +122,7 @@ impl Value {
Self::Dict(dict) => dict.at(&field).cloned(), Self::Dict(dict) => dict.at(&field).cloned(),
Self::Content(content) => content Self::Content(content) => content
.field(&field) .field(&field)
.cloned()
.ok_or_else(|| eco_format!("unknown field `{field}`")), .ok_or_else(|| eco_format!("unknown field `{field}`")),
Self::Module(module) => module.get(&field).cloned(), Self::Module(module) => module.get(&field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())), v => Err(eco_format!("cannot access fields on type {}", v.type_name())),
@ -241,60 +242,6 @@ impl Hash for Value {
} }
} }
impl From<i32> for Value {
fn from(v: i32) -> Self {
Self::Int(v as i64)
}
}
impl From<usize> for Value {
fn from(v: usize) -> Self {
Self::Int(v as i64)
}
}
impl From<Abs> for Value {
fn from(v: Abs) -> Self {
Self::Length(v.into())
}
}
impl From<Em> for Value {
fn from(v: Em) -> Self {
Self::Length(v.into())
}
}
impl From<RgbaColor> for Value {
fn from(v: RgbaColor) -> Self {
Self::Color(v.into())
}
}
impl From<&str> for Value {
fn from(v: &str) -> Self {
Self::Str(v.into())
}
}
impl From<EcoString> for Value {
fn from(v: EcoString) -> Self {
Self::Str(v.into())
}
}
impl From<String> for Value {
fn from(v: String) -> Self {
Self::Str(v.into())
}
}
impl From<Dynamic> for Value {
fn from(v: Dynamic) -> Self {
Self::Dyn(v)
}
}
/// A dynamic value. /// A dynamic value.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
pub struct Dynamic(Arc<dyn Bounds>); pub struct Dynamic(Arc<dyn Bounds>);
@ -336,6 +283,10 @@ impl PartialEq for Dynamic {
} }
} }
cast_to_value! {
v: Dynamic => Value::Dyn(v)
}
trait Bounds: Debug + Sync + Send + 'static { trait Bounds: Debug + Sync + Send + 'static {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
fn dyn_eq(&self, other: &Dynamic) -> bool; fn dyn_eq(&self, other: &Dynamic) -> bool;
@ -462,6 +413,7 @@ primitive! { Args: "arguments", Args }
mod tests { mod tests {
use super::*; use super::*;
use crate::eval::{array, dict}; use crate::eval::{array, dict};
use crate::geom::RgbaColor;
#[track_caller] #[track_caller]
fn test(value: impl Into<Value>, exp: &str) { fn test(value: impl Into<Value>, exp: &str) {

View File

@ -12,6 +12,7 @@ use std::sync::Arc;
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use crate::eval::{cast_from_value, cast_to_value, Value};
use crate::geom::Em; use crate::geom::Em;
use crate::util::Buffer; use crate::util::Buffer;
@ -249,3 +250,27 @@ pub enum VerticalFontMetric {
/// present and falls back to the descender from the `hhea` table otherwise. /// present and falls back to the descender from the `hhea` table otherwise.
Descender, Descender,
} }
cast_from_value! {
VerticalFontMetric,
/// The font's ascender, which typically exceeds the height of all glyphs.
"ascender" => Self::Ascender,
/// The approximate height of uppercase letters.
"cap-height" => Self::CapHeight,
/// The approximate height of non-ascending lowercase letters.
"x-height" => Self::XHeight,
/// The baseline on which the letters rest.
"baseline" => Self::Baseline,
/// The font's ascender, which typically exceeds the depth of all glyphs.
"descender" => Self::Descender,
}
cast_to_value! {
v: VerticalFontMetric => Value::from(match v {
VerticalFontMetric::Ascender => "ascender",
VerticalFontMetric::CapHeight => "cap-height",
VerticalFontMetric::XHeight => "x-height",
VerticalFontMetric::Baseline => "baseline" ,
VerticalFontMetric::Descender => "descender",
})
}

View File

@ -2,6 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::eval::{cast_from_value, cast_to_value, Value};
use crate::geom::Ratio;
/// Properties that distinguish a font from other fonts in the same family. /// Properties that distinguish a font from other fonts in the same family.
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -59,6 +62,24 @@ impl Default for FontStyle {
} }
} }
cast_from_value! {
FontStyle,
/// The default, typically upright style.
"normal" => Self::Normal,
/// A cursive style with custom letterform.
"italic" => Self::Italic,
/// Just a slanted version of the normal style.
"oblique" => Self::Oblique,
}
cast_to_value! {
v: FontStyle => Value::from(match v {
FontStyle::Normal => "normal",
FontStyle::Italic => "italic",
FontStyle::Oblique => "oblique",
})
}
/// The weight of a font. /// The weight of a font.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -127,6 +148,44 @@ impl Debug for FontWeight {
} }
} }
cast_from_value! {
FontWeight,
v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
/// Thin weight (100).
"thin" => Self::THIN,
/// Extra light weight (200).
"extralight" => Self::EXTRALIGHT,
/// Light weight (300).
"light" => Self::LIGHT,
/// Regular weight (400).
"regular" => Self::REGULAR,
/// Medium weight (500).
"medium" => Self::MEDIUM,
/// Semibold weight (600).
"semibold" => Self::SEMIBOLD,
/// Bold weight (700).
"bold" => Self::BOLD,
/// Extrabold weight (800).
"extrabold" => Self::EXTRABOLD,
/// Black weight (900).
"black" => Self::BLACK,
}
cast_to_value! {
v: FontWeight => Value::from(match v {
FontWeight::THIN => "thin",
FontWeight::EXTRALIGHT => "extralight",
FontWeight::LIGHT => "light",
FontWeight::REGULAR => "regular",
FontWeight::MEDIUM => "medium",
FontWeight::SEMIBOLD => "semibold",
FontWeight::BOLD => "bold",
FontWeight::EXTRABOLD => "extrabold",
FontWeight::BLACK => "black",
_ => return v.to_number().into(),
})
}
/// The width of a font. /// The width of a font.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -163,8 +222,8 @@ impl FontStretch {
/// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
/// necessary. /// necessary.
pub fn from_ratio(ratio: f32) -> Self { pub fn from_ratio(ratio: Ratio) -> Self {
Self((ratio.max(0.5).min(2.0) * 1000.0) as u16) Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16)
} }
/// Create a font stretch from an OpenType-style number between 1 and 9, /// Create a font stretch from an OpenType-style number between 1 and 9,
@ -184,12 +243,12 @@ impl FontStretch {
} }
/// The ratio between 0.5 and 2.0 corresponding to this stretch. /// The ratio between 0.5 and 2.0 corresponding to this stretch.
pub fn to_ratio(self) -> f32 { pub fn to_ratio(self) -> Ratio {
self.0 as f32 / 1000.0 Ratio::new(self.0 as f64 / 1000.0)
} }
/// The absolute ratio distance between this and another font stretch. /// The absolute ratio distance between this and another font stretch.
pub fn distance(self, other: Self) -> f32 { pub fn distance(self, other: Self) -> Ratio {
(self.to_ratio() - other.to_ratio()).abs() (self.to_ratio() - other.to_ratio()).abs()
} }
} }
@ -202,10 +261,19 @@ impl Default for FontStretch {
impl Debug for FontStretch { impl Debug for FontStretch {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}%", 100.0 * self.to_ratio()) self.to_ratio().fmt(f)
} }
} }
cast_from_value! {
FontStretch,
v: Ratio => Self::from_ratio(v),
}
cast_to_value! {
v: FontStretch => v.to_ratio().into()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -214,6 +214,10 @@ impl<'a> Sum<&'a Self> for Abs {
} }
} }
cast_to_value! {
v: Abs => Value::Length(v.into())
}
/// Different units of absolute measurement. /// Different units of absolute measurement.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum AbsUnit { pub enum AbsUnit {

View File

@ -115,3 +115,51 @@ impl Debug for GenAlign {
} }
} }
} }
cast_from_value! {
GenAlign: "alignment",
}
cast_from_value! {
Axes<GenAlign>: "2d alignment",
}
cast_from_value! {
Axes<Option<GenAlign>>,
align: GenAlign => {
let mut aligns = Axes::default();
aligns.set(align.axis(), Some(align));
aligns
},
aligns: Axes<GenAlign> => aligns.map(Some),
}
cast_to_value! {
v: Axes<Option<GenAlign>> => match (v.x, v.y) {
(Some(x), Some(y)) => Axes::new(x, y).into(),
(Some(x), None) => x.into(),
(None, Some(y)) => y.into(),
(None, None) => Value::None,
}
}
impl Resolve for GenAlign {
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,
}
}
}
impl Fold for GenAlign {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}

View File

@ -2,6 +2,7 @@ use std::any::Any;
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
use super::*; use super::*;
use crate::eval::Array;
/// A container with a horizontal and vertical component. /// A container with a horizontal and vertical component.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
@ -272,3 +273,37 @@ impl BitAndAssign for Axes<bool> {
self.y &= rhs.y; self.y &= rhs.y;
} }
} }
cast_from_value! {
Axes<Rel<Length>>,
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?,
}
},
}
cast_to_value! {
v: Axes<Rel<Length>> => Value::Array(array![v.x, v.y])
}
impl<T: Resolve> Resolve for Axes<T> {
type Output = Axes<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
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 {
Some(value) => value.fold(outer),
None => outer,
})
}
}

View File

@ -107,3 +107,100 @@ pub enum Corner {
/// The bottom left corner. /// The bottom left corner.
BottomLeft, BottomLeft,
} }
impl<T> Cast for Corners<Option<T>>
where
T: Cast + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let left = take("left")?.or(rest);
let top = take("top")?.or(rest);
let right = take("right")?.or(rest);
let bottom = take("bottom")?.or(rest);
let corners = Corners {
top_left: take("top-left")?.or(top).or(left),
top_right: take("top-right")?.or(top).or(right),
bottom_right: take("bottom-right")?.or(bottom).or(right),
bottom_left: take("bottom-left")?.or(bottom).or(left),
};
dict.finish(&[
"top-left",
"top-right",
"bottom-right",
"bottom-left",
"left",
"top",
"right",
"bottom",
"rest",
])?;
Ok(corners)
} else if T::is(&value) {
Ok(Self::splat(Some(T::cast(value)?)))
} else {
<Self as Cast>::error(value)
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("dictionary")
}
}
impl<T: Resolve> Resolve for Corners<T> {
type Output = Corners<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T: Fold> Fold for Corners<Option<T>> {
type Output = Corners<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| match inner {
Some(value) => value.fold(outer),
None => outer,
})
}
}
impl<T> From<Corners<Option<T>>> for Value
where
T: PartialEq + Into<Value>,
{
fn from(corners: Corners<Option<T>>) -> Self {
if corners.is_uniform() {
if let Some(value) = corners.top_left {
return value.into();
}
}
let mut dict = Dict::new();
if let Some(top_left) = corners.top_left {
dict.insert("top-left".into(), top_left.into());
}
if let Some(top_right) = corners.top_right {
dict.insert("top-right".into(), top_right.into());
}
if let Some(bottom_right) = corners.bottom_right {
dict.insert("bottom-right".into(), bottom_right.into());
}
if let Some(bottom_left) = corners.bottom_left {
dict.insert("bottom-left".into(), bottom_left.into());
}
Value::Dict(dict)
}
}

View File

@ -73,3 +73,7 @@ impl Debug for Dir {
}) })
} }
} }
cast_from_value! {
Dir: "direction",
}

View File

@ -134,3 +134,19 @@ impl Sum for Em {
Self(iter.map(|s| s.0).sum()) Self(iter.map(|s| s.0).sum())
} }
} }
cast_to_value! {
v: Em => Value::Length(v.into())
}
impl Resolve for Em {
type Output = Abs;
fn resolve(self, styles: StyleChain) -> Self::Output {
if self.is_zero() {
Abs::zero()
} else {
self.at(item!(em)(styles))
}
}
}

View File

@ -124,3 +124,11 @@ assign_impl!(Length += Length);
assign_impl!(Length -= Length); assign_impl!(Length -= Length);
assign_impl!(Length *= f64); assign_impl!(Length *= f64);
assign_impl!(Length /= f64); assign_impl!(Length /= f64);
impl Resolve for Length {
type Output = Abs;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.abs + self.em.resolve(styles)
}
}

View File

@ -19,6 +19,7 @@ mod ratio;
mod rel; mod rel;
mod rounded; mod rounded;
mod scalar; mod scalar;
mod shape;
mod sides; mod sides;
mod size; mod size;
mod smart; mod smart;
@ -42,6 +43,7 @@ pub use self::ratio::*;
pub use self::rel::*; pub use self::rel::*;
pub use self::rounded::*; pub use self::rounded::*;
pub use self::scalar::*; pub use self::scalar::*;
pub use self::shape::*;
pub use self::sides::*; pub use self::sides::*;
pub use self::size::*; pub use self::size::*;
pub use self::smart::*; pub use self::smart::*;
@ -55,6 +57,10 @@ use std::hash::{Hash, Hasher};
use std::iter::Sum; use std::iter::Sum;
use std::ops::*; use std::ops::*;
use crate::diag::StrResult;
use crate::eval::{array, cast_from_value, cast_to_value, Cast, CastInfo, Dict, Value};
use crate::model::{Fold, Resolve, StyleChain};
/// Generic access to a structure's components. /// Generic access to a structure's components.
pub trait Get<Index> { pub trait Get<Index> {
/// The structure's component type. /// The structure's component type.
@ -72,40 +78,6 @@ pub trait Get<Index> {
} }
} }
/// A geometric shape with optional fill and stroke.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Shape {
/// The shape's geometry.
pub geometry: Geometry,
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's border stroke.
pub stroke: Option<Stroke>,
}
/// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry {
/// A line to a point (relative to its position).
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
/// A bezier path.
Path(Path),
}
impl Geometry {
/// Fill the geometry without a stroke.
pub fn filled(self, fill: Paint) -> Shape {
Shape { geometry: self, fill: Some(fill), stroke: None }
}
/// Stroke the geometry without a fill.
pub fn stroked(self, stroke: Stroke) -> Shape {
Shape { geometry: self, fill: None, stroke: Some(stroke) }
}
}
/// A numeric type. /// A numeric type.
pub trait Numeric: pub trait Numeric:
Sized Sized

View File

@ -9,10 +9,7 @@ pub enum Paint {
Solid(Color), Solid(Color),
} }
impl<T> From<T> for Paint impl<T: Into<Color>> From<T> for Paint {
where
T: Into<Color>,
{
fn from(t: T) -> Self { fn from(t: T) -> Self {
Self::Solid(t.into()) Self::Solid(t.into())
} }
@ -26,6 +23,15 @@ impl Debug for Paint {
} }
} }
cast_from_value! {
Paint,
color: Color => Self::Solid(color),
}
cast_to_value! {
Paint::Solid(color): Paint => Value::Color(color)
}
/// A color in a dynamic format. /// A color in a dynamic format.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Color { pub enum Color {
@ -274,15 +280,16 @@ impl Debug for RgbaColor {
} }
} }
impl<T> From<T> for Color impl<T: Into<RgbaColor>> From<T> for Color {
where
T: Into<RgbaColor>,
{
fn from(rgba: T) -> Self { fn from(rgba: T) -> Self {
Self::Rgba(rgba.into()) Self::Rgba(rgba.into())
} }
} }
cast_to_value! {
v: RgbaColor => Value::Color(v.into())
}
/// An 8-bit CMYK color. /// An 8-bit CMYK color.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct CmykColor { pub struct CmykColor {

View File

@ -199,3 +199,31 @@ impl<T: Numeric> Add<Ratio> for Rel<T> {
self + Rel::from(other) self + Rel::from(other)
} }
} }
impl<T> Resolve for Rel<T>
where
T: Resolve + Numeric,
<T as Resolve>::Output: Numeric,
{
type Output = Rel<<T as Resolve>::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|abs| abs.resolve(styles))
}
}
impl Fold for Rel<Abs> {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}
impl Fold for Rel<Length> {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}

35
src/geom/shape.rs Normal file
View File

@ -0,0 +1,35 @@
use super::*;
/// A geometric shape with optional fill and stroke.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Shape {
/// The shape's geometry.
pub geometry: Geometry,
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's border stroke.
pub stroke: Option<Stroke>,
}
/// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry {
/// A line to a point (relative to its position).
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
/// A bezier path.
Path(Path),
}
impl Geometry {
/// Fill the geometry without a stroke.
pub fn filled(self, fill: Paint) -> Shape {
Shape { geometry: self, fill: Some(fill), stroke: None }
}
/// Stroke the geometry without a fill.
pub fn stroked(self, stroke: Stroke) -> Shape {
Shape { geometry: self, fill: None, stroke: Some(stroke) }
}
}

View File

@ -177,3 +177,88 @@ impl Side {
} }
} }
} }
impl<T> Cast for Sides<Option<T>>
where
T: Default + Cast + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let x = take("x")?.or(rest);
let y = take("y")?.or(rest);
let sides = Sides {
left: take("left")?.or(x),
top: take("top")?.or(y),
right: take("right")?.or(x),
bottom: take("bottom")?.or(y),
};
dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
Ok(sides)
} else if T::is(&value) {
Ok(Self::splat(Some(T::cast(value)?)))
} else {
<Self as Cast>::error(value)
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("dictionary")
}
}
impl<T> From<Sides<Option<T>>> for Value
where
T: PartialEq + Into<Value>,
{
fn from(sides: Sides<Option<T>>) -> Self {
if sides.is_uniform() {
if let Some(value) = sides.left {
return value.into();
}
}
let mut dict = Dict::new();
if let Some(left) = sides.left {
dict.insert("left".into(), left.into());
}
if let Some(top) = sides.top {
dict.insert("top".into(), top.into());
}
if let Some(right) = sides.right {
dict.insert("right".into(), right.into());
}
if let Some(bottom) = sides.bottom {
dict.insert("bottom".into(), bottom.into());
}
Value::Dict(dict)
}
}
impl<T: Resolve> Resolve for Sides<T> {
type Output = Sides<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T: Fold> Fold for Sides<Option<T>> {
type Output = Sides<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| match inner {
Some(value) => value.fold(outer),
None => outer,
})
}
}

View File

@ -31,6 +31,18 @@ impl<T> Smart<T> {
} }
} }
/// Map the contained custom value with `f` if it contains a custom value,
/// otherwise returns `default`.
pub fn map_or<F, U>(self, default: U, f: F) -> U
where
F: FnOnce(T) -> U,
{
match self {
Self::Auto => default,
Self::Custom(x) => f(x),
}
}
/// Keeps `self` if it contains a custom value, otherwise returns `other`. /// Keeps `self` if it contains a custom value, otherwise returns `other`.
pub fn or(self, other: Smart<T>) -> Self { pub fn or(self, other: Smart<T>) -> Self {
match self { match self {
@ -72,3 +84,50 @@ impl<T> Default for Smart<T> {
Self::Auto Self::Auto
} }
} }
impl<T: Cast> Cast for Smart<T> {
fn is(value: &Value) -> bool {
matches!(value, Value::Auto) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self::Auto),
v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)),
_ => <Self as Cast>::error(value),
}
}
fn describe() -> CastInfo {
T::describe() + CastInfo::Type("auto")
}
}
impl<T: Resolve> Resolve for Smart<T> {
type Output = Smart<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T> Fold for Smart<T>
where
T: Fold,
T::Output: Default,
{
type Output = Smart<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.map(|inner| inner.fold(outer.unwrap_or_default()))
}
}
impl<T: Into<Value>> From<Smart<T>> for Value {
fn from(v: Smart<T>) -> Self {
match v {
Smart::Custom(v) => v.into(),
Smart::Auto => Value::Auto,
}
}
}

View File

@ -58,3 +58,37 @@ impl<T: Debug> Debug for PartialStroke<T> {
} }
} }
} }
cast_from_value! {
PartialStroke: "stroke",
thickness: Length => Self {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
},
color: Color => Self {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
},
}
impl Resolve for PartialStroke {
type Output = PartialStroke<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
PartialStroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
}
}
}
impl Fold for PartialStroke<Abs> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
Self {
paint: self.paint.or(outer.paint),
thickness: self.thickness.or(outer.thickness),
}
}
}

View File

@ -338,6 +338,11 @@ 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);
}
}
Value::Dict(dict) => { Value::Dict(dict) => {
for (name, value) in dict.iter() { for (name, value) in dict.iter() {
ctx.value_completion(Some(name.clone().into()), value, false, None); ctx.value_completion(Some(name.clone().into()), value, false, None);

View File

@ -39,14 +39,13 @@ extern crate self as typst;
#[macro_use] #[macro_use]
pub mod util; pub mod util;
#[macro_use] #[macro_use]
pub mod geom;
#[macro_use]
pub mod diag; pub mod diag;
#[macro_use] #[macro_use]
pub mod eval; pub mod eval;
pub mod doc; pub mod doc;
pub mod export; pub mod export;
pub mod font; pub mod font;
pub mod geom;
pub mod ide; pub mod ide;
pub mod image; pub mod image;
pub mod model; pub mod model;

View File

@ -1,27 +1,24 @@
use std::any::{Any, TypeId}; use std::any::TypeId;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter, Write};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::iter::{self, Sum}; use std::iter::{self, Sum};
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign};
use std::sync::Arc;
use comemo::Tracked; use comemo::Tracked;
use ecow::{EcoString, EcoVec}; use ecow::{EcoString, EcoVec};
use siphasher::sip128::{Hasher128, SipHasher};
use typst_macros::node;
use super::{capability, capable, Guard, Key, Property, Recipe, Style, StyleMap}; use super::{node, Guard, Key, Property, Recipe, Style, StyleMap};
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::eval::{Args, ParamInfo, Value, Vm}; use crate::eval::{cast_from_value, Args, Cast, ParamInfo, Value, Vm};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::ReadableTypeId;
use crate::World; use crate::World;
/// Composable representation of styled content. /// Composable representation of styled content.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
pub struct Content { pub struct Content {
obj: Arc<dyn Bounds>, id: NodeId,
span: Option<Span>, span: Option<Span>,
fields: EcoVec<(EcoString, Value)>,
modifiers: EcoVec<Modifier>, modifiers: EcoVec<Modifier>,
} }
@ -30,55 +27,43 @@ pub struct Content {
enum Modifier { enum Modifier {
Prepared, Prepared,
Guard(Guard), Guard(Guard),
Label(Label),
Field(EcoString, Value),
} }
impl Content { impl Content {
pub fn new<T: Node>() -> Self {
Self {
id: T::id(),
span: None,
fields: EcoVec::new(),
modifiers: EcoVec::new(),
}
}
/// Create empty content. /// Create empty content.
pub fn empty() -> Self { pub fn empty() -> Self {
SequenceNode(vec![]).pack() SequenceNode::new(vec![]).pack()
} }
/// Create a new sequence node from multiples nodes. /// Create a new sequence node from multiples nodes.
pub fn sequence(seq: Vec<Self>) -> Self { pub fn sequence(seq: Vec<Self>) -> Self {
match seq.as_slice() { match seq.as_slice() {
[_] => seq.into_iter().next().unwrap(), [_] => seq.into_iter().next().unwrap(),
_ => SequenceNode(seq).pack(), _ => SequenceNode::new(seq).pack(),
} }
} }
/// Attach a span to the content. /// Attach a span to the content.
pub fn spanned(mut self, span: Span) -> Self { pub fn spanned(mut self, span: Span) -> Self {
if let Some(styled) = self.to_mut::<StyledNode>() { if let Some(styled) = self.to::<StyledNode>() {
styled.sub.span = Some(span); self = StyledNode::new(styled.sub().spanned(span), styled.map()).pack();
} else if let Some(styled) = self.to::<StyledNode>() {
self = StyledNode {
sub: styled.sub.clone().spanned(span),
map: styled.map.clone(),
}
.pack();
} }
self.span = Some(span); self.span = Some(span);
self self
} }
/// Attach a label to the content. /// Attach a label to the content.
pub fn labelled(mut self, label: Label) -> Self { pub fn labelled(self, label: Label) -> Self {
for (i, modifier) in self.modifiers.iter().enumerate() { self.with_field("label", label)
if matches!(modifier, Modifier::Label(_)) {
self.modifiers.make_mut()[i] = Modifier::Label(label);
return self;
}
}
self.modifiers.push(Modifier::Label(label));
self
}
/// Attach a field to the content.
pub fn push_field(&mut self, name: impl Into<EcoString>, value: Value) {
self.modifiers.push(Modifier::Field(name.into(), value));
} }
/// Style this content with a single style property. /// Style this content with a single style property.
@ -87,31 +72,21 @@ impl Content {
} }
/// Style this content with a style entry. /// Style this content with a style entry.
pub fn styled_with_entry(mut self, style: Style) -> Self { pub fn styled_with_entry(self, style: Style) -> Self {
if let Some(styled) = self.to_mut::<StyledNode>() { self.styled_with_map(style.into())
styled.map.apply_one(style);
self
} else if let Some(styled) = self.to::<StyledNode>() {
let mut map = styled.map.clone();
map.apply_one(style);
StyledNode { sub: styled.sub.clone(), map }.pack()
} else {
StyledNode { sub: self, map: style.into() }.pack()
}
} }
/// Style this content with a full style map. /// Style this content with a full style map.
pub fn styled_with_map(mut self, styles: StyleMap) -> Self { pub fn styled_with_map(self, styles: StyleMap) -> Self {
if styles.is_empty() { if styles.is_empty() {
return self; self
} else if let Some(styled) = self.to::<StyledNode>() {
let mut map = styled.map();
map.apply(styles);
StyledNode::new(styled.sub(), map).pack()
} else {
StyledNode::new(self, styles).pack()
} }
if let Some(styled) = self.to_mut::<StyledNode>() {
styled.map.apply(styles);
return self;
}
StyledNode { sub: self, map: styles }.pack()
} }
/// Style this content with a recipe, eagerly applying it if possible. /// Style this content with a recipe, eagerly applying it if possible.
@ -139,12 +114,12 @@ impl Content {
impl Content { impl Content {
/// The id of the contained node. /// The id of the contained node.
pub fn id(&self) -> NodeId { pub fn id(&self) -> NodeId {
(*self.obj).id() self.id
} }
/// The node's human-readable name. /// The node's human-readable name.
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
(*self.obj).name() self.id.name()
} }
/// The node's span. /// The node's span.
@ -154,72 +129,86 @@ impl Content {
/// The content's label. /// The content's label.
pub fn label(&self) -> Option<&Label> { pub fn label(&self) -> Option<&Label> {
self.modifiers.iter().find_map(|modifier| match modifier { match self.field("label")? {
Modifier::Label(label) => Some(label), Value::Label(label) => Some(label),
_ => None, _ => None,
})
}
/// Access a field on this content.
pub fn field(&self, name: &str) -> Option<Value> {
if name == "label" {
return Some(match self.label() {
Some(label) => Value::Label(label.clone()),
None => Value::None,
});
}
for modifier in &self.modifiers {
if let Modifier::Field(other, value) = modifier {
if name == other {
return Some(value.clone());
}
} }
} }
self.obj.field(name) pub fn with_field(
mut self,
name: impl Into<EcoString>,
value: impl Into<Value>,
) -> Self {
self.push_field(name, value);
self
}
/// Attach a field to the content.
pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) {
let name = name.into();
if let Some(i) = self.fields.iter().position(|(field, _)| *field == name) {
self.fields.make_mut()[i] = (name, value.into());
} else {
self.fields.push((name, value.into()));
}
}
pub fn field(&self, name: &str) -> Option<&Value> {
static NONE: Value = Value::None;
self.fields
.iter()
.find(|(field, _)| field == name)
.map(|(_, value)| value)
.or_else(|| (name == "label").then(|| &NONE))
}
pub fn fields(&self) -> &[(EcoString, Value)] {
&self.fields
}
#[track_caller]
pub fn cast_field<T: Cast>(&self, name: &str) -> T {
match self.field(name) {
Some(value) => value.clone().cast().unwrap(),
None => field_is_missing(name),
}
} }
/// Whether the contained node is of type `T`. /// Whether the contained node is of type `T`.
pub fn is<T>(&self) -> bool pub fn is<T>(&self) -> bool
where where
T: Capable + 'static, T: Node + 'static,
{ {
(*self.obj).as_any().is::<T>() self.id == NodeId::of::<T>()
} }
/// Cast to `T` if the contained node is of type `T`. /// Cast to `T` if the contained node is of type `T`.
pub fn to<T>(&self) -> Option<&T> pub fn to<T>(&self) -> Option<&T>
where where
T: Capable + 'static, T: Node + 'static,
{ {
(*self.obj).as_any().downcast_ref::<T>() self.is::<T>().then(|| unsafe { std::mem::transmute(self) })
} }
/// Whether this content has the given capability. /// Whether this content has the given capability.
pub fn has<C>(&self) -> bool pub fn has<C>(&self) -> bool
where where
C: Capability + ?Sized, C: ?Sized + 'static,
{ {
self.obj.vtable(TypeId::of::<C>()).is_some() (self.id.0.vtable)(TypeId::of::<C>()).is_some()
} }
/// Cast to a trait object if this content has the given capability. /// Cast to a trait object if this content has the given capability.
pub fn with<C>(&self) -> Option<&C> pub fn with<C>(&self) -> Option<&C>
where where
C: Capability + ?Sized, C: ?Sized + 'static,
{ {
let node: &dyn Bounds = &*self.obj; let vtable = (self.id.0.vtable)(TypeId::of::<C>())?;
let vtable = node.vtable(TypeId::of::<C>())?; let data = self as *const Self as *const ();
let data = node as *const dyn Bounds as *const ();
Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) })
} }
/// Try to cast to a mutable instance of `T`.
fn to_mut<T: 'static>(&mut self) -> Option<&mut T> {
Arc::get_mut(&mut self.obj)?.as_any_mut().downcast_mut::<T>()
}
/// Disable a show rule recipe. /// Disable a show rule recipe.
#[doc(hidden)] #[doc(hidden)]
pub fn guarded(mut self, id: Guard) -> Self { pub fn guarded(mut self, id: Guard) -> Self {
@ -262,12 +251,40 @@ impl Content {
pub(super) fn copy_modifiers(&mut self, from: &Content) { pub(super) fn copy_modifiers(&mut self, from: &Content) {
self.span = from.span; self.span = from.span;
self.modifiers = from.modifiers.clone(); self.modifiers = from.modifiers.clone();
if let Some(label) = from.label() {
self.push_field("label", label.clone())
}
} }
} }
impl Debug for Content { impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.obj.fmt(f) struct Pad<'a>(&'a str);
impl Debug for Pad<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(self.0)
}
}
if let Some(styled) = self.to::<StyledNode>() {
styled.map().fmt(f)?;
styled.sub().fmt(f)
} else if let Some(seq) = self.to::<SequenceNode>() {
f.debug_list().entries(&seq.children()).finish()
} else if self.id.name() == "space" {
' '.fmt(f)
} else if self.id.name() == "text" {
self.field("text").unwrap().fmt(f)
} else {
f.write_str(self.name())?;
if self.fields.is_empty() {
return Ok(());
}
f.write_char(' ')?;
f.debug_map()
.entries(self.fields.iter().map(|(name, value)| (Pad(name), value)))
.finish()
}
} }
} }
@ -280,27 +297,19 @@ impl Default for Content {
impl Add for Content { impl Add for Content {
type Output = Self; type Output = Self;
fn add(self, mut rhs: Self) -> Self::Output { fn add(self, rhs: Self) -> Self::Output {
let mut lhs = self; let lhs = self;
if let Some(lhs_mut) = lhs.to_mut::<SequenceNode>() {
if let Some(rhs_mut) = rhs.to_mut::<SequenceNode>() {
lhs_mut.0.append(&mut rhs_mut.0);
} else if let Some(rhs) = rhs.to::<SequenceNode>() {
lhs_mut.0.extend(rhs.0.iter().cloned());
} else {
lhs_mut.0.push(rhs);
}
return lhs;
}
let seq = match (lhs.to::<SequenceNode>(), rhs.to::<SequenceNode>()) { let seq = match (lhs.to::<SequenceNode>(), rhs.to::<SequenceNode>()) {
(Some(lhs), Some(rhs)) => lhs.0.iter().chain(&rhs.0).cloned().collect(), (Some(lhs), Some(rhs)) => {
(Some(lhs), None) => lhs.0.iter().cloned().chain(iter::once(rhs)).collect(), lhs.children().into_iter().chain(rhs.children()).collect()
(None, Some(rhs)) => iter::once(lhs).chain(rhs.0.iter().cloned()).collect(), }
(Some(lhs), None) => {
lhs.children().into_iter().chain(iter::once(rhs)).collect()
}
(None, Some(rhs)) => iter::once(lhs).chain(rhs.children()).collect(),
(None, None) => vec![lhs, rhs], (None, None) => vec![lhs, rhs],
}; };
SequenceNode::new(seq).pack()
SequenceNode(seq).pack()
} }
} }
@ -316,73 +325,33 @@ impl Sum for Content {
} }
} }
trait Bounds: Node + Debug + Sync + Send + 'static {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn hash128(&self) -> u128;
}
impl<T> Bounds for T
where
T: Node + Debug + Hash + Sync + Send + 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn hash128(&self) -> u128 {
let mut state = SipHasher::new();
self.type_id().hash(&mut state);
self.hash(&mut state);
state.finish128().as_u128()
}
}
impl Hash for dyn Bounds {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u128(self.hash128());
}
}
/// A node with applied styles. /// A node with applied styles.
#[capable] #[node]
#[derive(Clone, Hash)]
pub struct StyledNode { pub struct StyledNode {
/// The styled content. /// The styled content.
#[positional]
#[required]
pub sub: Content, pub sub: Content,
/// The styles. /// The styles.
#[positional]
#[required]
pub map: StyleMap, pub map: StyleMap,
} }
#[node] cast_from_value! {
impl StyledNode {} StyleMap: "style map",
impl Debug for StyledNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.map.fmt(f)?;
self.sub.fmt(f)
}
} }
/// A sequence of nodes. /// A sequence of nodes.
/// ///
/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in /// Combines other arbitrary content. So, when you write `[Hi] + [you]` in
/// Typst, the two text nodes are combined into a single sequence node. /// Typst, the two text nodes are combined into a single sequence node.
#[capable]
#[derive(Clone, Hash)]
pub struct SequenceNode(pub Vec<Content>);
#[node] #[node]
impl SequenceNode {} pub struct SequenceNode {
#[variadic]
impl Debug for SequenceNode { #[required]
fn fmt(&self, f: &mut Formatter) -> fmt::Result { pub children: Vec<Content>,
f.debug_list().entries(self.0.iter()).finish()
}
} }
/// A label for a node. /// A label for a node.
@ -396,80 +365,83 @@ impl Debug for Label {
} }
/// A constructable, stylable content node. /// A constructable, stylable content node.
pub trait Node: 'static + Capable { pub trait Node: Construct + Set + Sized + 'static {
/// The node's ID.
fn id() -> NodeId;
/// Pack a node into type-erased content. /// Pack a node into type-erased content.
fn pack(self) -> Content fn pack(self) -> Content;
where
Self: Node + Debug + Hash + Sync + Send + Sized + 'static,
{
Content {
obj: Arc::new(self),
span: None,
modifiers: EcoVec::new(),
}
}
/// A unique identifier of the node type.
fn id(&self) -> NodeId;
/// The node's name.
fn name(&self) -> &'static str;
/// Construct a node from the arguments.
///
/// This is passed only the arguments that remain after execution of the
/// node's set rule.
fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content>
where
Self: Sized;
/// Parse relevant arguments into style properties for this node.
///
/// When `constructor` is true, [`construct`](Self::construct) will run
/// after this invocation of `set` with the remaining arguments.
fn set(args: &mut Args, constructor: bool) -> SourceResult<StyleMap>
where
Self: Sized;
/// List the settable properties.
fn properties() -> Vec<ParamInfo>
where
Self: Sized;
/// Access a field on this node.
fn field(&self, name: &str) -> Option<Value>;
} }
/// A unique identifier for a node type. /// A unique identifier for a node.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone)]
pub struct NodeId(ReadableTypeId); pub struct NodeId(&'static NodeMeta);
impl NodeId { impl NodeId {
/// The id of the given node type. pub fn of<T: Node>() -> Self {
pub fn of<T: 'static>() -> Self { T::id()
Self(ReadableTypeId::of::<T>()) }
pub fn from_meta(meta: &'static NodeMeta) -> Self {
Self(meta)
}
/// The name of the identified node.
pub fn name(self) -> &'static str {
self.0.name
} }
} }
impl Debug for NodeId { impl Debug for NodeId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f) f.pad(self.name())
} }
} }
/// A capability a node can have. impl Hash for NodeId {
/// fn hash<H: Hasher>(&self, state: &mut H) {
/// Should be implemented by trait objects that are accessible through state.write_usize(self.0 as *const _ as usize);
/// [`Capable`]. }
pub trait Capability: 'static {} }
/// Dynamically access a trait implementation at runtime. impl Eq for NodeId {}
pub unsafe trait Capable {
/// Return the vtable pointer of the trait object with given type `id` impl PartialEq for NodeId {
/// if `self` implements the trait. fn eq(&self, other: &Self) -> bool {
fn vtable(&self, of: TypeId) -> Option<*const ()>; std::ptr::eq(self.0, other.0)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct NodeMeta {
pub name: &'static str,
pub vtable: fn(of: TypeId) -> Option<*const ()>,
}
pub trait Construct {
/// Construct a node from the arguments.
///
/// This is passed only the arguments that remain after execution of the
/// node's set rule.
fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content>;
}
pub trait Set {
/// Parse relevant arguments into style properties for this node.
///
/// When `constructor` is true, [`construct`](Construct::construct) will run
/// after this invocation of `set` with the remaining arguments.
fn set(args: &mut Args, constructor: bool) -> SourceResult<StyleMap>;
/// List the settable properties.
fn properties() -> Vec<ParamInfo>;
} }
/// Indicates that a node cannot be labelled. /// Indicates that a node cannot be labelled.
#[capability]
pub trait Unlabellable {} pub trait Unlabellable {}
#[cold]
#[track_caller]
fn field_is_missing(name: &str) -> ! {
panic!("required field `{name}` is missing")
}

View File

@ -13,4 +13,4 @@ pub use self::typeset::*;
#[doc(hidden)] #[doc(hidden)]
pub use once_cell; pub use once_cell;
pub use typst_macros::{capability, capable, node}; pub use typst_macros::node;

View File

@ -1,4 +1,4 @@
use super::{capability, Content, NodeId, Recipe, Selector, StyleChain, Vt}; use super::{Content, NodeId, Recipe, Selector, StyleChain, Vt};
use crate::diag::SourceResult; use crate::diag::SourceResult;
/// Whether the target is affected by show rules in the given style chain. /// Whether the target is affected by show rules in the given style chain.
@ -105,7 +105,7 @@ fn try_apply(
let mut result = vec![]; let mut result = vec![];
let mut cursor = 0; let mut cursor = 0;
for m in regex.find_iter(text) { for m in regex.find_iter(&text) {
let start = m.start(); let start = m.start();
if cursor < start { if cursor < start {
result.push(make(text[cursor..start].into())); result.push(make(text[cursor..start].into()));
@ -133,7 +133,6 @@ fn try_apply(
} }
/// Preparations before execution of any show rule. /// Preparations before execution of any show rule.
#[capability]
pub trait Prepare { pub trait Prepare {
/// Prepare the node for show rule application. /// Prepare the node for show rule application.
fn prepare( fn prepare(
@ -145,7 +144,6 @@ pub trait Prepare {
} }
/// The base recipe for a node. /// The base recipe for a node.
#[capability]
pub trait Show { pub trait Show {
/// Execute the base recipe for this node. /// Execute the base recipe for this node.
fn show( fn show(
@ -157,7 +155,6 @@ pub trait Show {
} }
/// Post-process a node after it was realized. /// Post-process a node after it was realized.
#[capability]
pub trait Finalize { pub trait Finalize {
/// Finalize the fully realized form of the node. Use this for effects that /// Finalize the fully realized form of the node. Use this for effects that
/// should work even in the face of a user-defined show rule, for example /// should work even in the face of a user-defined show rule, for example

View File

@ -1,21 +1,16 @@
use std::any::Any; use std::any::Any;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::{Hash, Hasher};
use std::iter; use std::iter;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc;
use comemo::{Prehashed, Tracked}; use comemo::Tracked;
use ecow::EcoString;
use super::{Content, Label, NodeId}; use super::{Content, Label, Node, NodeId};
use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::eval::{Args, Dict, Func, Regex, Value}; use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value};
use crate::geom::{
Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
Smart,
};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::ReadableTypeId;
use crate::World; use crate::World;
/// A map of style properties. /// A map of style properties.
@ -76,7 +71,7 @@ impl StyleMap {
/// Mark all contained properties as _scoped_. This means that they only /// Mark all contained properties as _scoped_. This means that they only
/// apply to the first descendant node (of their type) in the hierarchy and /// apply to the first descendant node (of their type) in the hierarchy and
/// not its children, too. This is used by /// not its children, too. This is used by
/// [constructors](super::Node::construct). /// [constructors](super::Construct::construct).
pub fn scoped(mut self) -> Self { pub fn scoped(mut self) -> Self {
for entry in &mut self.0 { for entry in &mut self.0 {
if let Style::Property(property) = entry { if let Style::Property(property) = entry {
@ -98,7 +93,7 @@ impl StyleMap {
/// Returns `Some(_)` with an optional span if this map contains styles for /// Returns `Some(_)` with an optional span if this map contains styles for
/// the given `node`. /// the given `node`.
pub fn interruption<T: 'static>(&self) -> Option<Option<Span>> { pub fn interruption<T: Node>(&self) -> Option<Option<Span>> {
let node = NodeId::of::<T>(); let node = NodeId::of::<T>();
self.0.iter().find_map(|entry| match entry { self.0.iter().find_map(|entry| match entry {
Style::Property(property) => property.is_of(node).then(|| property.origin), Style::Property(property) => property.is_of(node).then(|| property.origin),
@ -114,6 +109,12 @@ impl From<Style> for StyleMap {
} }
} }
impl PartialEq for StyleMap {
fn eq(&self, other: &Self) -> bool {
crate::util::hash128(self) == crate::util::hash128(other)
}
}
impl Debug for StyleMap { impl Debug for StyleMap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for entry in self.0.iter() { for entry in self.0.iter() {
@ -154,13 +155,11 @@ impl Style {
impl Debug for Style { impl Debug for Style {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("#[")?;
match self { match self {
Self::Property(property) => property.fmt(f)?, Self::Property(property) => property.fmt(f),
Self::Recipe(recipe) => recipe.fmt(f)?, Self::Recipe(recipe) => recipe.fmt(f),
Self::Barrier(id) => write!(f, "Barrier for {id:?}")?, Self::Barrier(id) => write!(f, "#[Barrier for {id:?}]"),
} }
f.write_str("]")
} }
} }
@ -175,12 +174,9 @@ pub struct Property {
/// hierarchy. Used by constructors. /// hierarchy. Used by constructors.
scoped: bool, scoped: bool,
/// The property's value. /// The property's value.
value: Arc<Prehashed<dyn Bounds>>, value: Value,
/// The span of the set rule the property stems from. /// The span of the set rule the property stems from.
origin: Option<Span>, origin: Option<Span>,
/// The name of the property.
#[cfg(debug_assertions)]
name: &'static str,
} }
impl Property { impl Property {
@ -189,11 +185,9 @@ impl Property {
Self { Self {
key: KeyId::of::<K>(), key: KeyId::of::<K>(),
node: K::node(), node: K::node(),
value: Arc::new(Prehashed::new(value)), value: value.into(),
scoped: false, scoped: false,
origin: None, origin: None,
#[cfg(debug_assertions)]
name: K::NAME,
} }
} }
@ -208,9 +202,12 @@ impl Property {
} }
/// Access the property's value if it is of the given key. /// Access the property's value if it is of the given key.
pub fn downcast<K: Key>(&self) -> Option<&K::Value> { #[track_caller]
pub fn cast<K: Key>(&self) -> Option<K::Value> {
if self.key == KeyId::of::<K>() { if self.key == KeyId::of::<K>() {
(**self.value).as_any().downcast_ref() Some(self.value.clone().cast().unwrap_or_else(|err| {
panic!("{} (for {} with value {:?})", err, self.key.name(), self.value)
}))
} else { } else {
None None
} }
@ -234,9 +231,7 @@ impl Property {
impl Debug for Property { impl Debug for Property {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[cfg(debug_assertions)] write!(f, "#set {}({}: {:?})", self.node.name(), self.key.name(), self.value)?;
write!(f, "{} = ", self.name)?;
write!(f, "{:?}", self.value)?;
if self.scoped { if self.scoped {
write!(f, " [scoped]")?; write!(f, " [scoped]")?;
} }
@ -267,47 +262,69 @@ where
/// A style property key. /// A style property key.
/// ///
/// This trait is not intended to be implemented manually, but rather through /// This trait is not intended to be implemented manually.
/// the `#[node]` proc-macro.
pub trait Key: Copy + 'static { pub trait Key: Copy + 'static {
/// The unfolded type which this property is stored as in a style map. /// The unfolded type which this property is stored as in a style map.
type Value: Debug + Clone + Hash + Sync + Send + 'static; type Value: Cast + Into<Value>;
/// The folded type of value that is returned when reading this property /// The folded type of value that is returned when reading this property
/// from a style chain. /// from a style chain.
type Output<'a>; type Output;
/// The name of the property, used for debug printing. /// The id of the property.
const NAME: &'static str; fn id() -> KeyId;
/// The id of the node the key belongs to. /// The id of the node the key belongs to.
fn node() -> NodeId; fn node() -> NodeId;
/// Compute an output value from a sequence of values belonging to this key, /// Compute an output value from a sequence of values belonging to this key,
/// folding if necessary. /// folding if necessary.
fn get<'a>( fn get(chain: StyleChain, values: impl Iterator<Item = Self::Value>) -> Self::Output;
chain: StyleChain<'a>,
values: impl Iterator<Item = &'a Self::Value>,
) -> Self::Output<'a>;
} }
/// A unique identifier for a property key. /// A unique identifier for a style key.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone)]
struct KeyId(ReadableTypeId); pub struct KeyId(&'static KeyMeta);
impl KeyId { impl KeyId {
/// The id of the given key.
pub fn of<T: Key>() -> Self { pub fn of<T: Key>() -> Self {
Self(ReadableTypeId::of::<T>()) T::id()
}
pub fn from_meta(meta: &'static KeyMeta) -> Self {
Self(meta)
}
pub fn name(self) -> &'static str {
self.0.name
} }
} }
impl Debug for KeyId { impl Debug for KeyId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f) f.pad(self.name())
} }
} }
impl Hash for KeyId {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_usize(self.0 as *const _ as usize);
}
}
impl Eq for KeyId {}
impl PartialEq for KeyId {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.0, other.0)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct KeyMeta {
pub name: &'static str,
}
/// A show rule recipe. /// A show rule recipe.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
pub struct Recipe { pub struct Recipe {
@ -362,7 +379,7 @@ impl Recipe {
impl Debug for Recipe { impl Debug for Recipe {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Recipe matching {:?}", self.selector) write!(f, "#show {:?}: {:?}", self.selector, self.transform)
} }
} }
@ -382,7 +399,7 @@ pub enum Selector {
impl Selector { impl Selector {
/// Define a simple node selector. /// Define a simple node selector.
pub fn node<T: 'static>() -> Self { pub fn node<T: Node>() -> Self {
Self::Node(NodeId::of::<T>(), None) Self::Node(NodeId::of::<T>(), None)
} }
@ -399,17 +416,25 @@ impl Selector {
&& dict && dict
.iter() .iter()
.flat_map(|dict| dict.iter()) .flat_map(|dict| dict.iter())
.all(|(name, value)| target.field(name).as_ref() == Some(value)) .all(|(name, value)| target.field(name) == Some(value))
} }
Self::Label(label) => target.label() == Some(label), Self::Label(label) => target.label() == Some(label),
Self::Regex(regex) => { Self::Regex(regex) => {
target.id() == item!(text_id) target.id() == item!(text_id)
&& item!(text_str)(target).map_or(false, |text| regex.is_match(text)) && item!(text_str)(target).map_or(false, |text| regex.is_match(&text))
} }
} }
} }
} }
cast_from_value! {
Selector: "selector",
text: EcoString => Self::text(&text),
label: Label => Self::Label(label),
func: Func => func.select(None)?,
regex: Regex => Self::Regex(regex),
}
/// A show rule transformation that can be applied to a match. /// A show rule transformation that can be applied to a match.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum Transform { pub enum Transform {
@ -421,6 +446,17 @@ pub enum Transform {
Style(StyleMap), Style(StyleMap),
} }
cast_from_value! {
Transform,
content: Content => Self::Content(content),
func: Func => {
if func.argc().map_or(false, |count| count != 1) {
Err("function must have exactly one parameter")?
}
Self::Func(func)
},
}
/// A chain of style maps, similar to a linked list. /// A chain of style maps, similar to a linked list.
/// ///
/// A style chain allows to combine properties from multiple style maps in a /// A style chain allows to combine properties from multiple style maps in a
@ -478,7 +514,7 @@ impl<'a> StyleChain<'a> {
/// Returns the property's default value if no map in the chain contains an /// Returns the property's default value if no map in the chain contains an
/// entry for it. Also takes care of resolving and folding and returns /// entry for it. Also takes care of resolving and folding and returns
/// references where applicable. /// references where applicable.
pub fn get<K: Key>(self, key: K) -> K::Output<'a> { pub fn get<K: Key>(self, key: K) -> K::Output {
K::get(self, self.values(key)) K::get(self, self.values(key))
} }
@ -534,10 +570,7 @@ impl Debug for StyleChain<'_> {
impl PartialEq for StyleChain<'_> { impl PartialEq for StyleChain<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
let as_ptr = |s| s as *const _; crate::util::hash128(self) == crate::util::hash128(other)
self.head.as_ptr() == other.head.as_ptr()
&& self.head.len() == other.head.len()
&& self.tail.map(as_ptr) == other.tail.map(as_ptr)
} }
} }
@ -585,13 +618,14 @@ struct Values<'a, K> {
} }
impl<'a, K: Key> Iterator for Values<'a, K> { impl<'a, K: Key> Iterator for Values<'a, K> {
type Item = &'a K::Value; type Item = K::Value;
#[track_caller]
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
for entry in &mut self.entries { for entry in &mut self.entries {
match entry { match entry {
Style::Property(property) => { Style::Property(property) => {
if let Some(value) = property.downcast::<K>() { if let Some(value) = property.cast::<K>() {
if !property.scoped() || self.barriers <= 1 { if !property.scoped() || self.barriers <= 1 {
return Some(value); return Some(value);
} }
@ -672,6 +706,20 @@ impl<T> StyleVec<T> {
} }
} }
impl StyleVec<Content> {
pub fn to_vec(self) -> Vec<Content> {
self.items
.into_iter()
.zip(
self.maps
.iter()
.flat_map(|(map, count)| iter::repeat(map).take(*count)),
)
.map(|(content, map)| content.styled_with_map(map.clone()))
.collect()
}
}
impl<T> Default for StyleVec<T> { impl<T> Default for StyleVec<T> {
fn default() -> Self { fn default() -> Self {
Self { items: vec![], maps: vec![] } Self { items: vec![], maps: vec![] }
@ -791,26 +839,6 @@ pub trait Resolve {
fn resolve(self, styles: StyleChain) -> Self::Output; fn resolve(self, styles: StyleChain) -> Self::Output;
} }
impl Resolve for Em {
type Output = Abs;
fn resolve(self, styles: StyleChain) -> Self::Output {
if self.is_zero() {
Abs::zero()
} else {
self.at(item!(em)(styles))
}
}
}
impl Resolve for Length {
type Output = Abs;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.abs + self.em.resolve(styles)
}
}
impl<T: Resolve> Resolve for Option<T> { impl<T: Resolve> Resolve for Option<T> {
type Output = Option<T::Output>; type Output = Option<T::Output>;
@ -819,74 +847,6 @@ impl<T: Resolve> Resolve for Option<T> {
} }
} }
impl<T: Resolve> Resolve for Smart<T> {
type Output = Smart<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T: Resolve> Resolve for Axes<T> {
type Output = Axes<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T: Resolve> Resolve for Sides<T> {
type Output = Sides<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T: Resolve> Resolve for Corners<T> {
type Output = Corners<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
}
}
impl<T> Resolve for Rel<T>
where
T: Resolve + Numeric,
<T as Resolve>::Output: Numeric,
{
type Output = Rel<<T as Resolve>::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|abs| abs.resolve(styles))
}
}
impl Resolve for GenAlign {
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,
}
}
}
impl Resolve for PartialStroke {
type Output = PartialStroke<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
PartialStroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
}
}
}
/// A property that is folded to determine its final value. /// A property that is folded to determine its final value.
pub trait Fold { pub trait Fold {
/// The type of the folded output. /// The type of the folded output.
@ -907,92 +867,3 @@ where
self.map(|inner| inner.fold(outer.unwrap_or_default())) self.map(|inner| inner.fold(outer.unwrap_or_default()))
} }
} }
impl<T> Fold for Smart<T>
where
T: Fold,
T::Output: Default,
{
type Output = Smart<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.map(|inner| inner.fold(outer.unwrap_or_default()))
}
}
impl<T> Fold for Axes<Option<T>>
where
T: Fold,
{
type Output = Axes<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| match inner {
Some(value) => value.fold(outer),
None => outer,
})
}
}
impl<T> Fold for Sides<Option<T>>
where
T: Fold,
{
type Output = Sides<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| match inner {
Some(value) => value.fold(outer),
None => outer,
})
}
}
impl<T> Fold for Corners<Option<T>>
where
T: Fold,
{
type Output = Corners<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| match inner {
Some(value) => value.fold(outer),
None => outer,
})
}
}
impl Fold for PartialStroke<Abs> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
Self {
paint: self.paint.or(outer.paint),
thickness: self.thickness.or(outer.thickness),
}
}
}
impl Fold for Rel<Length> {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}
impl Fold for Rel<Abs> {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}
impl Fold for GenAlign {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}

View File

@ -8,7 +8,6 @@ use comemo::{Track, Tracked, TrackedMut};
use super::{Content, Selector, StyleChain}; use super::{Content, Selector, StyleChain};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::doc::{Document, Element, Frame, Location, Meta}; use crate::doc::{Document, Element, Frame, Location, Meta};
use crate::eval::Value;
use crate::geom::Transform; use crate::geom::Transform;
use crate::util::hash128; use crate::util::hash128;
use crate::World; use crate::World;
@ -162,7 +161,7 @@ impl Introspector {
let pos = pos.transform(ts); let pos = pos.transform(ts);
let mut node = content.clone(); let mut node = content.clone();
let loc = Location { page, pos }; let loc = Location { page, pos };
node.push_field("loc", Value::Dict(loc.encode())); node.push_field("loc", loc);
self.nodes.push((id, node)); self.nodes.push((id, node));
} }
} }

View File

@ -60,10 +60,7 @@ pub trait ArcExt<T> {
fn take(self) -> T; fn take(self) -> T;
} }
impl<T> ArcExt<T> for Arc<T> impl<T: Clone> ArcExt<T> for Arc<T> {
where
T: Clone,
{
fn take(self) -> T { fn take(self) -> T {
match Arc::try_unwrap(self) { match Arc::try_unwrap(self) {
Ok(v) => v, Ok(v) => v,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -146,9 +146,8 @@ impl Args {
} }
fn library() -> Library { fn library() -> Library {
/// # Test /// Category: test
/// ## Category /// Display: Test
/// test
#[func] #[func]
fn test(args: &mut typst::eval::Args) -> SourceResult<Value> { fn test(args: &mut typst::eval::Args) -> SourceResult<Value> {
let lhs = args.expect::<Value>("left-hand side")?; let lhs = args.expect::<Value>("left-hand side")?;
@ -159,9 +158,8 @@ fn library() -> Library {
Ok(Value::None) Ok(Value::None)
} }
/// # Print /// Category: test
/// ## Category /// Display: Print
/// test
#[func] #[func]
fn print(args: &mut typst::eval::Args) -> SourceResult<Value> { fn print(args: &mut typst::eval::Args) -> SourceResult<Value> {
print!("> "); print!("> ");

View File

@ -2,7 +2,7 @@
--- ---
// Override lists. // Override lists.
#show list: it => "(" + it.items.join(", ") + ")" #show list: it => "(" + it.items.map(item => item.body).join(", ") + ")"
- A - A
- B - B

View File

@ -35,7 +35,7 @@
#show terms: it => table( #show terms: it => table(
columns: 2, columns: 2,
inset: 3pt, inset: 3pt,
..it.items.map(item => (emph(item.at(0)), item.at(1))).flatten(), ..it.items.map(item => (emph(item.term), item.description)).flatten(),
) )
/ A: One letter / A: One letter

View File

@ -17,7 +17,7 @@ $ x = (-b plus.minus sqrt(b^2 - 4a c))/(2a) $
$ binom(circle, square) $ $ binom(circle, square) $
--- ---
// Error: 8-13 missing argument: lower index // Error: 8-13 missing argument: lower
$ binom(x^2) $ $ binom(x^2) $
--- ---

Some files were not shown because too many files have changed in this diff Show More