Fully untyped model
This commit is contained in:
parent
6ab7760822
commit
25b5bd1175
@ -1,3 +1,5 @@
|
||||
//! Calculations and processing of numeric values.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Rem;
|
||||
|
||||
@ -6,7 +8,7 @@ use typst::eval::{Module, Scope};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A module with computational functions.
|
||||
pub fn calc() -> Module {
|
||||
pub fn module() -> Module {
|
||||
let mut scope = Scope::new();
|
||||
scope.def_func::<AbsFunc>("abs");
|
||||
scope.def_func::<PowFunc>("pow");
|
||||
@ -37,7 +39,6 @@ pub fn calc() -> Module {
|
||||
Module::new("calc").with_scope(scope)
|
||||
}
|
||||
|
||||
/// # Absolute
|
||||
/// Calculate the absolute value of a numeric value.
|
||||
///
|
||||
/// ## Example
|
||||
@ -51,8 +52,8 @@ pub fn calc() -> Module {
|
||||
/// - value: `ToAbs` (positional, required)
|
||||
/// The value whose absolute value to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Absolute
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn abs(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct ToAbs(Value);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ToAbs,
|
||||
v: i64 => Self(Value::Int(v.abs())),
|
||||
v: f64 => Self(Value::Float(v.abs())),
|
||||
@ -72,7 +73,6 @@ castable! {
|
||||
v: Fr => Self(Value::Fraction(v.abs())),
|
||||
}
|
||||
|
||||
/// # Power
|
||||
/// Raise a value to some exponent.
|
||||
///
|
||||
/// ## Example
|
||||
@ -86,8 +86,8 @@ castable! {
|
||||
/// - exponent: `Num` (positional, required)
|
||||
/// The exponent of the power. Must be non-negative.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Power
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn pow(args: &mut Args) -> SourceResult<Value> {
|
||||
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))
|
||||
}
|
||||
|
||||
/// # Square Root
|
||||
/// Calculate the square root of a number.
|
||||
///
|
||||
/// ## Example
|
||||
@ -116,8 +115,8 @@ pub fn pow(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - value: `Num` (positional, required)
|
||||
/// The number whose square root to calculate. Must be non-negative.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Square Root
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn sqrt(args: &mut Args) -> SourceResult<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()))
|
||||
}
|
||||
|
||||
/// # Sine
|
||||
/// Calculate the sine of an angle.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose sine to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Sine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn sin(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose cosine to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Cosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn cos(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose tangent to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Tangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn tan(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -226,8 +221,8 @@ pub fn tan(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - value: `Num` (positional, required)
|
||||
/// The number whose arcsine to calculate. Must be between -1 and 1.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Arcsine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn asin(args: &mut Args) -> SourceResult<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())))
|
||||
}
|
||||
|
||||
/// # Arccosine
|
||||
/// Calculate the arccosine of a number.
|
||||
///
|
||||
/// ## Example
|
||||
@ -251,8 +245,8 @@ pub fn asin(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - value: `Num` (positional, required)
|
||||
/// The number whose arccosine to calculate. Must be between -1 and 1.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Arccosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn acos(args: &mut Args) -> SourceResult<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())))
|
||||
}
|
||||
|
||||
/// # Arctangent
|
||||
/// Calculate the arctangent of a number.
|
||||
///
|
||||
/// ## Example
|
||||
@ -276,15 +269,14 @@ pub fn acos(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - value: `Num` (positional, required)
|
||||
/// The number whose arctangent to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Arctangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn atan(args: &mut Args) -> SourceResult<Value> {
|
||||
let value = args.expect::<Num>("value")?;
|
||||
Ok(Value::Angle(Angle::rad(value.float().atan())))
|
||||
}
|
||||
|
||||
/// # Hyperbolic sine
|
||||
/// Calculate the hyperbolic sine of an angle.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose hyperbolic sine to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Hyperbolic sine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn sinh(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose hyperbolic cosine to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Hyperbolic cosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn cosh(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
/// The angle whose hyperbolic tangent to calculate.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Hyperbolic tangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn tanh(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
/// The base of the logarithm.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Logarithm
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn log(args: &mut Args) -> SourceResult<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)))
|
||||
}
|
||||
|
||||
/// # Round down
|
||||
/// Round a number down to the nearest integer.
|
||||
///
|
||||
/// 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)
|
||||
/// The number to round down.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Round down
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn floor(args: &mut Args) -> SourceResult<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.
|
||||
///
|
||||
/// 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)
|
||||
/// The number to round up.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Round up
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn ceil(args: &mut Args) -> SourceResult<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.
|
||||
///
|
||||
/// 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.
|
||||
/// - digits: `i64` (named)
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Round
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn round(args: &mut Args) -> SourceResult<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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -495,8 +480,8 @@ pub fn round(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - max: `Num` (positional, required)
|
||||
/// The inclusive maximum value.
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Clamp
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn clamp(args: &mut Args) -> SourceResult<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))
|
||||
}
|
||||
|
||||
/// # Minimum
|
||||
/// Determine the minimum of a sequence of values.
|
||||
///
|
||||
/// ## Example
|
||||
@ -524,14 +508,13 @@ pub fn clamp(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Minimum
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn min(args: &mut Args) -> SourceResult<Value> {
|
||||
minmax(args, Ordering::Less)
|
||||
}
|
||||
|
||||
/// # Maximum
|
||||
/// Determine the maximum of a sequence of values.
|
||||
///
|
||||
/// ## Example
|
||||
@ -547,8 +530,8 @@ pub fn min(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Maximum
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn max(args: &mut Args) -> SourceResult<Value> {
|
||||
minmax(args, Ordering::Greater)
|
||||
@ -575,7 +558,6 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
|
||||
Ok(extremum)
|
||||
}
|
||||
|
||||
/// # Even
|
||||
/// Determine whether an integer is even.
|
||||
///
|
||||
/// ## Example
|
||||
@ -591,14 +573,13 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: boolean
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Even
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn even(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(Value::Bool(args.expect::<i64>("value")? % 2 == 0))
|
||||
}
|
||||
|
||||
/// # Odd
|
||||
/// Determine whether an integer is odd.
|
||||
///
|
||||
/// ## Example
|
||||
@ -615,14 +596,13 @@ pub fn even(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: boolean
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Odd
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn odd(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(Value::Bool(args.expect::<i64>("value")? % 2 != 0))
|
||||
}
|
||||
|
||||
/// # Modulus
|
||||
/// Calculate the modulus of two numbers.
|
||||
///
|
||||
/// ## Example
|
||||
@ -640,8 +620,8 @@ pub fn odd(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: integer or float
|
||||
///
|
||||
/// ## Category
|
||||
/// calculate
|
||||
/// Display: Modulus
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn mod_(args: &mut Args) -> SourceResult<Value> {
|
||||
let dividend = args.expect::<Num>("dividend")?;
|
||||
@ -693,7 +673,7 @@ impl Num {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Num,
|
||||
v: i64 => Self::Int(v),
|
||||
v: f64 => Self::Float(v),
|
||||
@ -706,7 +686,7 @@ enum AngleLike {
|
||||
Angle(Angle),
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
AngleLike,
|
||||
v: i64 => Self::Int(v),
|
||||
v: f64 => Self::Float(v),
|
||||
|
@ -5,7 +5,6 @@ use typst::eval::Regex;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Integer
|
||||
/// Convert a value to an integer.
|
||||
///
|
||||
/// - Booleans are converted to `0` or `1`.
|
||||
@ -26,8 +25,8 @@ use crate::prelude::*;
|
||||
///
|
||||
/// - returns: integer
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Integer
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn int(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct ToInt(i64);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ToInt,
|
||||
v: bool => Self(v as i64),
|
||||
v: i64 => Self(v),
|
||||
@ -44,7 +43,6 @@ castable! {
|
||||
v: EcoString => Self(v.parse().map_err(|_| "not a valid integer")?),
|
||||
}
|
||||
|
||||
/// # Float
|
||||
/// Convert a value to a float.
|
||||
///
|
||||
/// - Booleans are converted to `0.0` or `1.0`.
|
||||
@ -67,8 +65,8 @@ castable! {
|
||||
///
|
||||
/// - returns: float
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Float
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn float(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct ToFloat(f64);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ToFloat,
|
||||
v: bool => Self(v as i64 as f64),
|
||||
v: i64 => Self(v as f64),
|
||||
@ -85,7 +83,6 @@ castable! {
|
||||
v: EcoString => Self(v.parse().map_err(|_| "not a valid float")?),
|
||||
}
|
||||
|
||||
/// # Luma
|
||||
/// Create a grayscale color.
|
||||
///
|
||||
/// ## Example
|
||||
@ -101,15 +98,14 @@ castable! {
|
||||
///
|
||||
/// - returns: color
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Luma
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn luma(args: &mut Args) -> SourceResult<Value> {
|
||||
let Component(luma) = args.expect("gray component")?;
|
||||
Ok(Value::Color(LumaColor::new(luma).into()))
|
||||
}
|
||||
|
||||
/// # RGBA
|
||||
/// Create an RGB(A) color.
|
||||
///
|
||||
/// The color is specified in the sRGB color space.
|
||||
@ -154,8 +150,8 @@ pub fn luma(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: color
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: RGBA
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn rgb(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct Component(u8);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Component,
|
||||
v: i64 => match v {
|
||||
0 ..= 255 => Self(v as u8),
|
||||
@ -188,7 +184,6 @@ castable! {
|
||||
},
|
||||
}
|
||||
|
||||
/// # CMYK
|
||||
/// Create a CMYK color.
|
||||
///
|
||||
/// This is useful if you want to target a specific printer. The conversion
|
||||
@ -217,8 +212,8 @@ castable! {
|
||||
///
|
||||
/// - returns: color
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: CMYK
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn cmyk(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct RatioComponent(u8);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
RatioComponent,
|
||||
v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
|
||||
Self((v.get() * 255.0).round() as u8)
|
||||
@ -240,7 +235,6 @@ castable! {
|
||||
},
|
||||
}
|
||||
|
||||
/// # Symbol
|
||||
/// Create a custom symbol with modifiers.
|
||||
///
|
||||
/// ## Example
|
||||
@ -272,8 +266,8 @@ castable! {
|
||||
///
|
||||
/// - returns: symbol
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Symbol
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn symbol(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct Variant(EcoString, char);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Variant,
|
||||
c: char => Self(EcoString::new(), c),
|
||||
array: Array => {
|
||||
@ -301,7 +295,6 @@ castable! {
|
||||
},
|
||||
}
|
||||
|
||||
/// # String
|
||||
/// Convert a value to a string.
|
||||
///
|
||||
/// - Integers are formatted in base 10.
|
||||
@ -322,8 +315,8 @@ castable! {
|
||||
///
|
||||
/// - returns: string
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: String
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn str(args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct ToStr(Str);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ToStr,
|
||||
v: i64 => Self(format_str!("{}", v)),
|
||||
v: f64 => Self(format_str!("{}", v)),
|
||||
@ -340,7 +333,6 @@ castable! {
|
||||
v: Str => Self(v),
|
||||
}
|
||||
|
||||
/// # Label
|
||||
/// Create a label from a string.
|
||||
///
|
||||
/// Inserting a label into content attaches it to the closest previous element
|
||||
@ -366,14 +358,13 @@ castable! {
|
||||
///
|
||||
/// - returns: label
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Label
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn label(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(Value::Label(Label(args.expect("string")?)))
|
||||
}
|
||||
|
||||
/// # Regex
|
||||
/// Create a regular expression from a string.
|
||||
///
|
||||
/// The result can be used as a
|
||||
@ -406,15 +397,14 @@ pub fn label(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: regex
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Regex
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn regex(args: &mut Args) -> SourceResult<Value> {
|
||||
let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
|
||||
Ok(Regex::new(&v).at(span)?.into())
|
||||
}
|
||||
|
||||
/// # Range
|
||||
/// Create an array consisting of a sequence of numbers.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// construct
|
||||
/// Display: Range
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn range(args: &mut Args) -> SourceResult<Value> {
|
||||
let first = args.expect::<i64>("end")?;
|
||||
|
@ -4,7 +4,6 @@ use typst::diag::{format_xml_like_error, FileError};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Plain text
|
||||
/// Read plain text from a file.
|
||||
///
|
||||
/// The file will be read and returned as a string.
|
||||
@ -23,8 +22,8 @@ use crate::prelude::*;
|
||||
///
|
||||
/// - returns: string
|
||||
///
|
||||
/// ## Category
|
||||
/// data-loading
|
||||
/// Display: Plain text
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn read(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
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()))
|
||||
}
|
||||
|
||||
/// # CSV
|
||||
/// Read structured data from a CSV file.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// data-loading
|
||||
/// Display: CSV
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
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.
|
||||
struct Delimiter(u8);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Delimiter,
|
||||
v: EcoString => {
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// data-loading
|
||||
/// Display: JSON
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn json(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
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())
|
||||
}
|
||||
|
||||
/// # XML
|
||||
/// Read structured data from an XML file.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// data-loading
|
||||
/// Display: XML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn xml(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } =
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Type
|
||||
/// Determine a value's type.
|
||||
///
|
||||
/// Returns the name of the value's type.
|
||||
@ -21,14 +20,13 @@ use crate::prelude::*;
|
||||
///
|
||||
/// - returns: string
|
||||
///
|
||||
/// ## Category
|
||||
/// foundations
|
||||
/// Display: Type
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn type_(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(args.expect::<Value>("value")?.type_name().into())
|
||||
}
|
||||
|
||||
/// # Representation
|
||||
/// The string representation of a value.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// foundations
|
||||
/// Display: Representation
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn repr(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(args.expect::<Value>("value")?.repr().into())
|
||||
}
|
||||
|
||||
/// # Panic
|
||||
/// Fail with an error.
|
||||
///
|
||||
/// ## Example
|
||||
@ -69,8 +66,8 @@ pub fn repr(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - payload: `Value` (positional)
|
||||
/// The value (or message) to panic with.
|
||||
///
|
||||
/// ## Category
|
||||
/// foundations
|
||||
/// Display: Panic
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn panic(args: &mut Args) -> SourceResult<Value> {
|
||||
match args.eat::<Value>()? {
|
||||
@ -79,7 +76,6 @@ pub fn panic(args: &mut Args) -> SourceResult<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Assert
|
||||
/// Ensure that a condition is fulfilled.
|
||||
///
|
||||
/// 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)
|
||||
/// The error message when the assertion fails.
|
||||
///
|
||||
/// ## Category
|
||||
/// foundations
|
||||
/// Display: Assert
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn assert(args: &mut Args) -> SourceResult<Value> {
|
||||
let check = args.expect::<bool>("condition")?;
|
||||
@ -112,7 +108,6 @@ pub fn assert(args: &mut Args) -> SourceResult<Value> {
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
/// # Evaluate
|
||||
/// Evaluate a string as Typst code.
|
||||
///
|
||||
/// This function should only be used as a last resort.
|
||||
@ -132,8 +127,8 @@ pub fn assert(args: &mut Args) -> SourceResult<Value> {
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// ## Category
|
||||
/// foundations
|
||||
/// Display: Evaluate
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?;
|
||||
|
@ -1,11 +1,10 @@
|
||||
//! Computational functions.
|
||||
|
||||
mod calc;
|
||||
pub mod calc;
|
||||
mod construct;
|
||||
mod data;
|
||||
mod foundations;
|
||||
|
||||
pub use self::calc::*;
|
||||
pub use self::construct::*;
|
||||
pub use self::data::*;
|
||||
pub use self::foundations::*;
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Align
|
||||
/// Align content horizontally and vertically.
|
||||
///
|
||||
/// ## Example
|
||||
@ -13,63 +12,59 @@ use crate::prelude::*;
|
||||
/// A work of art, a visual throne
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to align.
|
||||
///
|
||||
/// - 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 {}
|
||||
/// Display: Align
|
||||
/// Category: layout
|
||||
#[node(Show)]
|
||||
#[set({
|
||||
let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default();
|
||||
styles.set(Self::ALIGNMENT, aligns);
|
||||
})]
|
||||
pub struct AlignNode {
|
||||
/// The content to align.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl AlignNode {
|
||||
/// The alignment.
|
||||
#[property(fold, skip)]
|
||||
pub const ALIGNS: Axes<Option<GenAlign>> =
|
||||
Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top));
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
args.expect("body")
|
||||
}
|
||||
|
||||
fn set(...) {
|
||||
let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default();
|
||||
styles.set(Self::ALIGNS, aligns);
|
||||
impl Show for AlignNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.body())
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
/// # Columns
|
||||
/// Separate a region into multiple equally sized columns.
|
||||
///
|
||||
/// The `column` function allows to separate the interior of any container into
|
||||
@ -31,39 +30,25 @@ use crate::text::TextNode;
|
||||
/// variety of problems.
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - count: `usize` (positional, required)
|
||||
/// The number of columns.
|
||||
///
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content that should be layouted into the columns.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Columns
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct ColumnsNode {
|
||||
/// How many columns there should be.
|
||||
/// The number of columns.
|
||||
#[positional]
|
||||
#[required]
|
||||
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,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl ColumnsNode {
|
||||
/// The size of the gutter space between each column.
|
||||
#[property(resolve)]
|
||||
pub const GUTTER: Rel<Length> = Ratio::new(0.04).into();
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self {
|
||||
count: args.expect("column count")?,
|
||||
body: args.expect("body")?,
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Ratio::new(0.04).into())]
|
||||
pub gutter: Rel<Length>,
|
||||
}
|
||||
|
||||
impl Layout for ColumnsNode {
|
||||
@ -73,14 +58,16 @@ impl Layout for ColumnsNode {
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let body = self.body();
|
||||
|
||||
// Separating the infinite space into infinite columns does not make
|
||||
// much sense.
|
||||
if !regions.size.x.is_finite() {
|
||||
return self.body.layout(vt, styles, regions);
|
||||
return body.layout(vt, styles, regions);
|
||||
}
|
||||
|
||||
// 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 width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
|
||||
|
||||
@ -100,7 +87,7 @@ impl Layout for ColumnsNode {
|
||||
};
|
||||
|
||||
// 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 dir = styles.get(TextNode::DIR);
|
||||
@ -140,7 +127,6 @@ impl Layout for ColumnsNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Column Break
|
||||
/// A forced column break.
|
||||
///
|
||||
/// 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.
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - weak: `bool` (named)
|
||||
/// If `{true}`, the column break is skipped if the current column is already
|
||||
/// empty.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Behave)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Column Break
|
||||
/// Category: layout
|
||||
#[node(Behave)]
|
||||
pub struct ColbreakNode {
|
||||
/// If `{true}`, the column break is skipped if the current column is
|
||||
/// already empty.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
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 {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
if self.weak {
|
||||
if self.weak() {
|
||||
Behaviour::Weak(1)
|
||||
} else {
|
||||
Behaviour::Destructive
|
||||
|
@ -2,7 +2,6 @@ use super::VNode;
|
||||
use crate::layout::Spacing;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Box
|
||||
/// An inline-level container that sizes content.
|
||||
///
|
||||
/// All elements except inline math, text, and boxes are block-level and cannot
|
||||
@ -20,69 +19,75 @@ use crate::prelude::*;
|
||||
/// for more information.
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional)
|
||||
/// The contents of the box.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Box
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct BoxNode {
|
||||
/// The box's content.
|
||||
/// The contents of the box.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub body: Content,
|
||||
/// The box's width.
|
||||
pub width: Sizing,
|
||||
/// The box's height.
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl BoxNode {
|
||||
/// 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.
|
||||
/// ```
|
||||
#[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.
|
||||
///
|
||||
/// ```example
|
||||
/// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const BASELINE: Rel<Length> = Rel::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub baseline: Rel<Length>,
|
||||
|
||||
/// The box's background color. See the
|
||||
/// [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
|
||||
/// [rectangle's documentation]($func/rect.stroke) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None);
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Sides<Option<Option<PartialStroke>>>,
|
||||
|
||||
/// How much to round the box's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the box's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the box's size without affecting the layout.
|
||||
///
|
||||
@ -98,15 +103,11 @@ impl BoxNode {
|
||||
/// outset: (y: 3pt),
|
||||
/// radius: 2pt,
|
||||
/// )[rectangle].
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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())
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
}
|
||||
|
||||
impl Layout for BoxNode {
|
||||
@ -116,14 +117,14 @@ impl Layout for BoxNode {
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let width = match self.width {
|
||||
let width = match self.width() {
|
||||
Sizing::Auto => Smart::Auto,
|
||||
Sizing::Rel(rel) => Smart::Custom(rel),
|
||||
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
|
||||
};
|
||||
|
||||
// Resolve the sizing to a concrete size.
|
||||
let sizing = Axes::new(width, self.height);
|
||||
let sizing = Axes::new(width, self.height());
|
||||
let expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let size = sizing
|
||||
.resolve(styles)
|
||||
@ -132,10 +133,10 @@ impl Layout for BoxNode {
|
||||
.unwrap_or(regions.base());
|
||||
|
||||
// Apply inset.
|
||||
let mut child = self.body.clone();
|
||||
let mut child = self.body();
|
||||
let inset = styles.get(Self::INSET);
|
||||
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
|
||||
@ -169,7 +170,6 @@ impl Layout for BoxNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Block
|
||||
/// A block-level container.
|
||||
///
|
||||
/// Such a container can be used to separate content, size it and give it a
|
||||
@ -201,37 +201,6 @@ impl Layout for BoxNode {
|
||||
/// ```
|
||||
///
|
||||
/// ## 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)
|
||||
/// The spacing around this block. This is shorthand to set `above` and
|
||||
/// `below` to the same value.
|
||||
@ -245,35 +214,62 @@ impl Layout for BoxNode {
|
||||
/// A second paragraph.
|
||||
/// ```
|
||||
///
|
||||
/// - above: `Spacing` (named, settable)
|
||||
/// The spacing between this block and its predecessor. Takes precedence over
|
||||
/// `spacing`. Can be used in combination with a show rule to adjust the
|
||||
/// spacing around arbitrary block-level elements.
|
||||
///
|
||||
/// The default value is `{1.2em}`.
|
||||
///
|
||||
/// - below: `Spacing` (named, settable)
|
||||
/// The spacing between this block and its successor. Takes precedence
|
||||
/// over `spacing`.
|
||||
///
|
||||
/// The default value is `{1.2em}`.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Block
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
#[set({
|
||||
let spacing = args.named("spacing")?;
|
||||
styles.set_opt(
|
||||
Self::ABOVE,
|
||||
args.named("above")?
|
||||
.map(VNode::block_around)
|
||||
.or_else(|| spacing.map(VNode::block_spacing)),
|
||||
);
|
||||
styles.set_opt(
|
||||
Self::BELOW,
|
||||
args.named("below")?
|
||||
.map(VNode::block_around)
|
||||
.or_else(|| spacing.map(VNode::block_spacing)),
|
||||
);
|
||||
})]
|
||||
pub struct BlockNode {
|
||||
/// The block's content.
|
||||
/// The contents of the block.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub body: Content,
|
||||
/// The box's width.
|
||||
pub width: Smart<Rel<Length>>,
|
||||
/// The box's height.
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl BlockNode {
|
||||
/// The block's width.
|
||||
///
|
||||
/// ```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.
|
||||
///
|
||||
/// Defaults to `{true}`.
|
||||
@ -286,64 +282,74 @@ impl BlockNode {
|
||||
/// lorem(15),
|
||||
/// )
|
||||
/// ```
|
||||
pub const BREAKABLE: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub breakable: bool,
|
||||
|
||||
/// The block's background color. See the
|
||||
/// [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
|
||||
/// [rectangle's documentation]($func/rect.stroke) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None);
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Sides<Option<Option<PartialStroke>>>,
|
||||
|
||||
/// How much to round the block's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the block's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the block's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// The spacing between the previous and this block.
|
||||
#[property(skip)]
|
||||
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
|
||||
/// The spacing between this block and its predecessor. Takes precedence over
|
||||
/// `spacing`. Can be used in combination with a show rule to adjust the
|
||||
/// spacing around arbitrary block-level elements.
|
||||
///
|
||||
/// 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.
|
||||
#[property(skip)]
|
||||
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
|
||||
/// The spacing between this block and its successor. Takes precedence
|
||||
/// over `spacing`.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Use this to prevent page breaks between e.g. a heading and its body.
|
||||
#[property(skip)]
|
||||
pub const STICKY: bool = false;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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),
|
||||
);
|
||||
}
|
||||
#[settable]
|
||||
#[skip]
|
||||
#[default(false)]
|
||||
pub sticky: bool,
|
||||
}
|
||||
|
||||
impl Layout for BlockNode {
|
||||
@ -354,14 +360,14 @@ impl Layout for BlockNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Apply inset.
|
||||
let mut child = self.body.clone();
|
||||
let mut child = self.body();
|
||||
let inset = styles.get(Self::INSET);
|
||||
if inset.iter().any(|v| !v.is_zero()) {
|
||||
child = child.clone().padded(inset.map(|side| side.map(Length::from)));
|
||||
}
|
||||
|
||||
// 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 size = sizing
|
||||
.resolve(styles)
|
||||
@ -372,7 +378,7 @@ impl Layout for BlockNode {
|
||||
// Layout the child.
|
||||
let mut frames = if styles.get(Self::BREAKABLE) {
|
||||
// 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 frame = child.layout(vt, styles, pod)?.into_frame();
|
||||
size.x = frame.width();
|
||||
@ -385,7 +391,7 @@ impl Layout for BlockNode {
|
||||
|
||||
// Generate backlog for fixed height.
|
||||
let mut heights = vec![];
|
||||
if self.height.is_custom() {
|
||||
if sizing.y.is_custom() {
|
||||
let mut remaining = size.y;
|
||||
for region in regions.iter() {
|
||||
let limited = region.y.min(remaining);
|
||||
@ -454,18 +460,6 @@ impl Sizing {
|
||||
pub fn is_fractional(self) -> bool {
|
||||
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 {
|
||||
@ -474,11 +468,26 @@ impl Default for Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Spacing> for Sizing {
|
||||
fn from(spacing: Spacing) -> Self {
|
||||
match spacing {
|
||||
impl<T: Into<Spacing>> From<T> for Sizing {
|
||||
fn from(spacing: T) -> Self {
|
||||
match spacing.into() {
|
||||
Spacing::Rel(rel) => Self::Rel(rel),
|
||||
Spacing::Fr(fr) => Self::Fr(fr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cast_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),
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
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::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
/// # Numbered List
|
||||
use super::GridLayouter;
|
||||
|
||||
/// A numbered list.
|
||||
///
|
||||
/// Displays a sequence of items vertically and numbers them consecutively.
|
||||
@ -89,20 +90,19 @@ use crate::text::TextNode;
|
||||
/// items.
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Numbered List
|
||||
/// Category: layout
|
||||
#[node(Construct, Layout)]
|
||||
pub struct EnumNode {
|
||||
/// If true, the items are separated by leading instead of list spacing.
|
||||
pub tight: bool,
|
||||
/// The individual numbered items.
|
||||
pub items: StyleVec<(Option<NonZeroUsize>, Content)>,
|
||||
}
|
||||
/// The numbered list's items.
|
||||
#[variadic]
|
||||
pub items: Vec<EnumItem>,
|
||||
|
||||
/// 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
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
///
|
||||
@ -122,9 +122,9 @@ impl EnumNode {
|
||||
/// + Superscript
|
||||
/// + Numbering!
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const NUMBERING: Numbering =
|
||||
Numbering::Pattern(NumberingPattern::from_str("1.").unwrap());
|
||||
#[settable]
|
||||
#[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
|
||||
pub numbering: Numbering,
|
||||
|
||||
/// Whether to display the full numbering, including the numbers of
|
||||
/// all parent enumerations.
|
||||
@ -138,63 +138,51 @@ impl EnumNode {
|
||||
/// + Add integredients
|
||||
/// + Eat
|
||||
/// ```
|
||||
pub const FULL: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub full: bool,
|
||||
|
||||
/// The indentation of each item's label.
|
||||
#[property(resolve)]
|
||||
pub const INDENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub indent: Length,
|
||||
|
||||
/// The space between the numbering and the body of each item.
|
||||
#[property(resolve)]
|
||||
pub const BODY_INDENT: Length = Em::new(0.5).into();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Em::new(0.5).into())]
|
||||
pub body_indent: Length,
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) enumeration.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
pub const SPACING: Smart<Spacing> = Smart::Auto;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub spacing: Smart<Spacing>,
|
||||
|
||||
/// The numbers of parent items.
|
||||
#[property(skip, fold)]
|
||||
const PARENTS: Parent = vec![];
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default]
|
||||
parents: Parent,
|
||||
}
|
||||
|
||||
impl Construct for EnumNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let mut number: NonZeroUsize =
|
||||
args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap());
|
||||
|
||||
Ok(Self {
|
||||
tight: args.named("tight")?.unwrap_or(true),
|
||||
items: args
|
||||
.all()?
|
||||
.into_iter()
|
||||
.map(|body| {
|
||||
let item = (Some(number), body);
|
||||
number = number.saturating_add(1);
|
||||
item
|
||||
})
|
||||
.collect(),
|
||||
let mut items = args.all::<EnumItem>()?;
|
||||
if let Some(number) = args.named::<NonZeroUsize>("start")? {
|
||||
if let Some(first) = items.first_mut() {
|
||||
if first.number().is_none() {
|
||||
*first = EnumItem::new(first.body()).with_number(Some(number));
|
||||
}
|
||||
}
|
||||
}
|
||||
.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,
|
||||
}
|
||||
Ok(Self::new(items)
|
||||
.with_tight(args.named("tight")?.unwrap_or(true))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,12 +196,12 @@ impl Layout for EnumNode {
|
||||
let numbering = styles.get(Self::NUMBERING);
|
||||
let indent = styles.get(Self::INDENT);
|
||||
let body_indent = styles.get(Self::BODY_INDENT);
|
||||
let gutter = if self.tight {
|
||||
let gutter = if self.tight() {
|
||||
styles.get(ParNode::LEADING).into()
|
||||
} else {
|
||||
styles
|
||||
.get(Self::SPACING)
|
||||
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount)
|
||||
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount())
|
||||
};
|
||||
|
||||
let mut cells = vec![];
|
||||
@ -221,8 +209,8 @@ impl Layout for EnumNode {
|
||||
let mut parents = styles.get(Self::PARENTS);
|
||||
let full = styles.get(Self::FULL);
|
||||
|
||||
for ((n, item), map) in self.items.iter() {
|
||||
number = n.unwrap_or(number);
|
||||
for item in self.items() {
|
||||
number = item.number().unwrap_or(number);
|
||||
|
||||
let resolved = if full {
|
||||
parents.push(number);
|
||||
@ -230,7 +218,7 @@ impl Layout for EnumNode {
|
||||
parents.pop();
|
||||
content
|
||||
} else {
|
||||
match numbering {
|
||||
match &numbering {
|
||||
Numbering::Pattern(pattern) => {
|
||||
TextNode::packed(pattern.apply_kth(parents.len(), number))
|
||||
}
|
||||
@ -239,33 +227,68 @@ impl Layout for EnumNode {
|
||||
};
|
||||
|
||||
cells.push(Content::empty());
|
||||
cells.push(resolved.styled_with_map(map.clone()));
|
||||
cells.push(resolved);
|
||||
cells.push(Content::empty());
|
||||
cells.push(
|
||||
item.clone()
|
||||
.styled_with_map(map.clone())
|
||||
.styled(Self::PARENTS, Parent(number)),
|
||||
);
|
||||
cells.push(item.body().styled(Self::PARENTS, Parent(number)));
|
||||
number = number.saturating_add(1);
|
||||
}
|
||||
|
||||
GridNode {
|
||||
tracks: Axes::with_x(vec![
|
||||
let layouter = GridLayouter::new(
|
||||
vt,
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
gutter: Axes::with_y(vec![gutter.into()]),
|
||||
cells,
|
||||
}
|
||||
.layout(vt, styles, regions)
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
&cells,
|
||||
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);
|
||||
|
||||
cast_from_value! {
|
||||
Parent,
|
||||
v: NonZeroUsize => Self(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Parent => v.0.into()
|
||||
}
|
||||
|
||||
impl Fold for Parent {
|
||||
type Output = Vec<NonZeroUsize>;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use typst::model::Style;
|
||||
use typst::model::{Style, StyledNode};
|
||||
|
||||
use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode};
|
||||
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
|
||||
/// the contents of boxes.
|
||||
#[capable(Layout)]
|
||||
#[derive(Hash)]
|
||||
pub struct FlowNode(pub StyleVec<Content>);
|
||||
|
||||
#[node]
|
||||
impl FlowNode {}
|
||||
#[node(Layout)]
|
||||
pub struct FlowNode {
|
||||
/// The children that will be arranges into a flow.
|
||||
#[variadic]
|
||||
pub children: Vec<Content>,
|
||||
}
|
||||
|
||||
impl Layout for FlowNode {
|
||||
fn layout(
|
||||
@ -24,9 +24,17 @@ impl Layout for FlowNode {
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut layouter = FlowLayouter::new(regions);
|
||||
|
||||
for (child, map) in self.0.iter() {
|
||||
let styles = styles.chain(&map);
|
||||
if let Some(&node) = child.to::<VNode>() {
|
||||
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(node) = child.to::<VNode>() {
|
||||
layouter.layout_spacing(node, styles);
|
||||
} else if let Some(node) = child.to::<ParNode>() {
|
||||
let barrier = Style::Barrier(child.id());
|
||||
@ -40,16 +48,16 @@ impl Layout for FlowNode {
|
||||
{
|
||||
let barrier = Style::Barrier(child.id());
|
||||
let styles = styles.chain_one(&barrier);
|
||||
layouter.layout_single(vt, child, styles)?;
|
||||
layouter.layout_single(vt, &child, styles)?;
|
||||
} else if child.has::<dyn Layout>() {
|
||||
layouter.layout_multiple(vt, child, styles)?;
|
||||
layouter.layout_multiple(vt, &child, styles)?;
|
||||
} else if child.is::<ColbreakNode>() {
|
||||
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
|
||||
{
|
||||
layouter.finish_region();
|
||||
}
|
||||
} else {
|
||||
panic!("unexpected flow child: {child:?}");
|
||||
} else if let Some(span) = child.span() {
|
||||
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.
|
||||
struct FlowLayouter<'a> {
|
||||
/// The regions to layout children into.
|
||||
@ -113,11 +114,11 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
|
||||
/// Layout vertical spacing.
|
||||
fn layout_spacing(&mut self, node: VNode, styles: StyleChain) {
|
||||
self.layout_item(match node.amount {
|
||||
fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) {
|
||||
self.layout_item(match node.amount() {
|
||||
Spacing::Rel(v) => FlowItem::Absolute(
|
||||
v.resolve(styles).relative_to(self.initial.y),
|
||||
node.weakness > 0,
|
||||
node.weakness() > 0,
|
||||
),
|
||||
Spacing::Fr(v) => FlowItem::Fractional(v),
|
||||
});
|
||||
@ -130,7 +131,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
par: &ParNode,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
|
||||
let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles);
|
||||
let leading = styles.get(ParNode::LEADING);
|
||||
let consecutive = self.last_was_par;
|
||||
let frames = par
|
||||
@ -176,7 +177,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
content: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
|
||||
let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles);
|
||||
let sticky = styles.get(BlockNode::STICKY);
|
||||
let pod = Regions::one(self.regions.base(), Axes::splat(false));
|
||||
let layoutable = content.with::<dyn Layout>().unwrap();
|
||||
@ -204,7 +205,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
|
||||
// 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.
|
||||
let sticky = styles.get(BlockNode::STICKY);
|
||||
|
@ -3,7 +3,6 @@ use crate::text::TextNode;
|
||||
|
||||
use super::Sizing;
|
||||
|
||||
/// # Grid
|
||||
/// Arrange content in a grid.
|
||||
///
|
||||
/// The grid element allows you to arrange content in a grid. You can define the
|
||||
@ -61,64 +60,50 @@ use super::Sizing;
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - cells: `Content` (positional, variadic) The contents of the table cells.
|
||||
///
|
||||
/// 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.
|
||||
/// - gutter: `TrackSizings` (named)
|
||||
/// Defines the gaps between rows & columns.
|
||||
///
|
||||
/// If there are more gutters than defined sizes, the last gutter is repeated.
|
||||
///
|
||||
/// - column-gutter: `TrackSizings` (named) Defines the gaps between columns.
|
||||
/// Takes precedence over `gutter`.
|
||||
///
|
||||
/// - row-gutter: `TrackSizings` (named) Defines the gaps between rows. Takes
|
||||
/// precedence over `gutter`.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Grid
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct GridNode {
|
||||
/// Defines sizing for content rows and columns.
|
||||
pub tracks: Axes<Vec<Sizing>>,
|
||||
/// Defines sizing of gutter rows and columns between content.
|
||||
pub gutter: Axes<Vec<Sizing>>,
|
||||
/// The content to be arranged in a grid.
|
||||
/// The contents of the table cells.
|
||||
///
|
||||
/// The cells are populated in row-major order.
|
||||
#[variadic]
|
||||
pub cells: Vec<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl GridNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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())
|
||||
}
|
||||
/// 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.
|
||||
#[named]
|
||||
#[default]
|
||||
pub columns: TrackSizings,
|
||||
|
||||
/// 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.
|
||||
#[named]
|
||||
#[default]
|
||||
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 {
|
||||
@ -129,11 +114,12 @@ impl Layout for GridNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Prepare grid layout by unifying content and gutter tracks.
|
||||
let cells = self.cells();
|
||||
let layouter = GridLayouter::new(
|
||||
vt,
|
||||
self.tracks.as_deref(),
|
||||
self.gutter.as_deref(),
|
||||
&self.cells,
|
||||
Axes::new(&self.columns().0, &self.rows().0),
|
||||
Axes::new(&self.column_gutter().0, &self.row_gutter().0),
|
||||
&cells,
|
||||
regions,
|
||||
styles,
|
||||
);
|
||||
@ -147,18 +133,15 @@ impl Layout for GridNode {
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TrackSizings(pub Vec<Sizing>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
TrackSizings,
|
||||
sizing: Sizing => Self(vec![sizing]),
|
||||
count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]),
|
||||
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Sizing,
|
||||
_: AutoValue => Self::Auto,
|
||||
v: Rel<Length> => Self::Rel(v),
|
||||
v: Fr => Self::Fr(v),
|
||||
cast_to_value! {
|
||||
v: TrackSizings => v.0.into()
|
||||
}
|
||||
|
||||
/// Performs grid layout.
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Hide
|
||||
/// Hide content without affecting layout.
|
||||
///
|
||||
/// The `hide` function allows you to hide content while the layout still 'sees'
|
||||
@ -14,26 +13,18 @@ use crate::prelude::*;
|
||||
/// #hide[Hello] Joe
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to hide.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Hide
|
||||
/// Category: layout
|
||||
#[node(Show)]
|
||||
pub struct HideNode {
|
||||
/// The content to hide.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl Show for HideNode {
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing};
|
||||
use crate::layout::{BlockNode, ParNode, Sizing, Spacing};
|
||||
use crate::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
/// # Bullet List
|
||||
use super::GridLayouter;
|
||||
|
||||
/// A bullet list.
|
||||
///
|
||||
/// 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
|
||||
/// more than an item's hyphen becomes part of that item.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - items: `Content` (positional, variadic)
|
||||
/// The list's children.
|
||||
///
|
||||
/// When using the list syntax, adjacent items are automatically collected
|
||||
/// into lists, even through constructs like for loops.
|
||||
///
|
||||
/// ```example
|
||||
/// #for letter in "ABC" [
|
||||
/// - Letter #letter
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Bullet List
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct ListNode {
|
||||
/// If true, the items are separated by leading instead of list spacing.
|
||||
pub tight: bool,
|
||||
/// The individual bulleted or numbered items.
|
||||
pub items: StyleVec<Content>,
|
||||
}
|
||||
/// The bullet list's children.
|
||||
///
|
||||
/// When using the list syntax, adjacent items are automatically collected
|
||||
/// into lists, even through constructs like for loops.
|
||||
///
|
||||
/// ```example
|
||||
/// #for letter in "ABC" [
|
||||
/// - Letter #letter
|
||||
/// ]
|
||||
/// ```
|
||||
#[variadic]
|
||||
pub 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.
|
||||
///
|
||||
/// Instead of plain content, you can also pass an array with multiple
|
||||
@ -96,43 +89,35 @@ impl ListNode {
|
||||
/// - Items
|
||||
/// - Items
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const MARKER: Marker = Marker::Content(vec![]);
|
||||
#[settable]
|
||||
#[default(ListMarker::Content(vec![]))]
|
||||
pub marker: ListMarker,
|
||||
|
||||
/// The indent of each item's marker.
|
||||
#[property(resolve)]
|
||||
pub const INDENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub indent: Length,
|
||||
|
||||
/// The spacing between the marker and the body of each item.
|
||||
#[property(resolve)]
|
||||
pub const BODY_INDENT: Length = Em::new(0.5).into();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Em::new(0.5).into())]
|
||||
pub body_indent: Length,
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) list.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
pub const SPACING: Smart<Spacing> = Smart::Auto;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub spacing: Smart<Spacing>,
|
||||
|
||||
/// The nesting depth.
|
||||
#[property(skip, fold)]
|
||||
const DEPTH: Depth = 0;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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().cloned().map(Value::Content).collect(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default]
|
||||
depth: Depth,
|
||||
}
|
||||
|
||||
impl Layout for ListNode {
|
||||
@ -144,49 +129,65 @@ impl Layout for ListNode {
|
||||
) -> SourceResult<Fragment> {
|
||||
let indent = styles.get(Self::INDENT);
|
||||
let body_indent = styles.get(Self::BODY_INDENT);
|
||||
let gutter = if self.tight {
|
||||
let gutter = if self.tight() {
|
||||
styles.get(ParNode::LEADING).into()
|
||||
} else {
|
||||
styles
|
||||
.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 marker = styles.get(Self::MARKER).resolve(vt.world(), depth)?;
|
||||
|
||||
let mut cells = vec![];
|
||||
for (item, map) in self.items.iter() {
|
||||
for item in self.items() {
|
||||
cells.push(Content::empty());
|
||||
cells.push(marker.clone());
|
||||
cells.push(Content::empty());
|
||||
cells.push(
|
||||
item.clone().styled_with_map(map.clone()).styled(Self::DEPTH, Depth),
|
||||
);
|
||||
cells.push(item.body().styled(Self::DEPTH, Depth));
|
||||
}
|
||||
|
||||
GridNode {
|
||||
tracks: Axes::with_x(vec![
|
||||
let layouter = GridLayouter::new(
|
||||
vt,
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
gutter: Axes::with_y(vec![gutter.into()]),
|
||||
cells,
|
||||
}
|
||||
.layout(vt, styles, regions)
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
&cells,
|
||||
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.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Marker {
|
||||
pub enum ListMarker {
|
||||
Content(Vec<Content>),
|
||||
Func(Func),
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
impl ListMarker {
|
||||
/// Resolve the marker for the given depth.
|
||||
fn resolve(&self, world: Tracked<dyn World>, depth: usize) -> SourceResult<Content> {
|
||||
Ok(match self {
|
||||
@ -203,8 +204,8 @@ impl Marker {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
Marker,
|
||||
cast_from_value! {
|
||||
ListMarker,
|
||||
v: Content => Self::Content(vec![v]),
|
||||
array: Array => {
|
||||
if array.len() == 0 {
|
||||
@ -215,14 +216,28 @@ castable! {
|
||||
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;
|
||||
|
||||
cast_from_value! {
|
||||
Depth,
|
||||
_: Value => Self,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
_: Depth => Value::None
|
||||
}
|
||||
|
||||
impl Fold for Depth {
|
||||
type Output = usize;
|
||||
|
||||
fn fold(self, mut outer: Self::Output) -> Self::Output {
|
||||
outer += 1;
|
||||
outer
|
||||
fn fold(self, outer: Self::Output) -> Self::Output {
|
||||
outer + 1
|
||||
}
|
||||
}
|
||||
|
@ -48,8 +48,8 @@ use std::mem;
|
||||
use typed_arena::Arena;
|
||||
use typst::diag::SourceResult;
|
||||
use typst::model::{
|
||||
applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain,
|
||||
StyleVecBuilder, StyledNode,
|
||||
applicable, realize, Content, Node, SequenceNode, Style, StyleChain, StyleVecBuilder,
|
||||
StyledNode,
|
||||
};
|
||||
|
||||
use crate::math::{FormulaNode, LayoutMath};
|
||||
@ -60,7 +60,6 @@ use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode};
|
||||
use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode};
|
||||
|
||||
/// Root-level layout.
|
||||
#[capability]
|
||||
pub trait LayoutRoot {
|
||||
/// Layout into one frame per page.
|
||||
fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>;
|
||||
@ -96,7 +95,6 @@ impl LayoutRoot for Content {
|
||||
}
|
||||
|
||||
/// Layout into regions.
|
||||
#[capability]
|
||||
pub trait Layout {
|
||||
/// Layout into one frame per region.
|
||||
fn layout(
|
||||
@ -160,7 +158,7 @@ fn realize_root<'a>(
|
||||
builder.accept(content, styles)?;
|
||||
builder.interrupt_page(Some(styles))?;
|
||||
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.
|
||||
@ -185,7 +183,7 @@ fn realize_block<'a>(
|
||||
builder.accept(content, styles)?;
|
||||
builder.interrupt_par()?;
|
||||
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.
|
||||
@ -211,6 +209,7 @@ struct Scratch<'a> {
|
||||
styles: Arena<StyleChain<'a>>,
|
||||
/// An arena where intermediate content resulting from show rules is stored.
|
||||
content: Arena<Content>,
|
||||
maps: Arena<StyleMap>,
|
||||
}
|
||||
|
||||
impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
@ -231,10 +230,8 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<()> {
|
||||
if content.has::<dyn LayoutMath>() && !content.is::<FormulaNode>() {
|
||||
content = self
|
||||
.scratch
|
||||
.content
|
||||
.alloc(FormulaNode { body: content.clone(), block: false }.pack());
|
||||
content =
|
||||
self.scratch.content.alloc(FormulaNode::new(content.clone()).pack());
|
||||
}
|
||||
|
||||
// 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>() {
|
||||
for sub in &seq.0 {
|
||||
self.accept(sub, styles)?;
|
||||
for sub in seq.children() {
|
||||
let stored = self.scratch.content.alloc(sub);
|
||||
self.accept(stored, styles)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@ -269,8 +267,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
|
||||
self.interrupt_list()?;
|
||||
|
||||
if content.is::<ListItem>() {
|
||||
self.list.accept(content, styles);
|
||||
if self.list.accept(content, styles) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -286,7 +283,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
|
||||
let keep = content
|
||||
.to::<PagebreakNode>()
|
||||
.map_or(false, |pagebreak| !pagebreak.weak);
|
||||
.map_or(false, |pagebreak| !pagebreak.weak());
|
||||
|
||||
self.interrupt_page(keep.then(|| styles))?;
|
||||
|
||||
@ -308,11 +305,13 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
styled: &'a StyledNode,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<()> {
|
||||
let map = self.scratch.maps.alloc(styled.map());
|
||||
let stored = self.scratch.styles.alloc(styles);
|
||||
let styles = stored.chain(&styled.map);
|
||||
self.interrupt_style(&styled.map, None)?;
|
||||
self.accept(&styled.sub, styles)?;
|
||||
self.interrupt_style(&styled.map, Some(styles))?;
|
||||
let content = self.scratch.content.alloc(styled.sub());
|
||||
let styles = stored.chain(map);
|
||||
self.interrupt_style(&map, None)?;
|
||||
self.accept(content, styles)?;
|
||||
self.interrupt_style(map, Some(styles))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -381,7 +380,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
let (flow, shared) = mem::take(&mut self.flow).0.finish();
|
||||
let styles =
|
||||
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);
|
||||
self.accept(stored, styles)?;
|
||||
}
|
||||
@ -392,7 +391,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
/// Accepts pagebreaks and pages.
|
||||
struct DocBuilder<'a> {
|
||||
/// 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.
|
||||
keep_next: bool,
|
||||
}
|
||||
@ -400,12 +399,12 @@ struct DocBuilder<'a> {
|
||||
impl<'a> DocBuilder<'a> {
|
||||
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
|
||||
if let Some(pagebreak) = content.to::<PagebreakNode>() {
|
||||
self.keep_next = !pagebreak.weak;
|
||||
self.keep_next = !pagebreak.weak();
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(page) = content.to::<PageNode>() {
|
||||
self.pages.push(page.clone(), styles);
|
||||
if content.is::<PageNode>() {
|
||||
self.pages.push(content.clone(), styles);
|
||||
self.keep_next = false;
|
||||
return true;
|
||||
}
|
||||
@ -441,11 +440,11 @@ impl<'a> FlowBuilder<'a> {
|
||||
|
||||
if content.has::<dyn Layout>() || content.is::<ParNode>() {
|
||||
let is_tight_list = if let Some(node) = content.to::<ListNode>() {
|
||||
node.tight
|
||||
node.tight()
|
||||
} else if let Some(node) = content.to::<EnumNode>() {
|
||||
node.tight
|
||||
node.tight()
|
||||
} else if let Some(node) = content.to::<TermsNode>() {
|
||||
node.tight
|
||||
node.tight()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@ -458,9 +457,9 @@ impl<'a> FlowBuilder<'a> {
|
||||
|
||||
let above = styles.get(BlockNode::ABOVE);
|
||||
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(below.pack(), styles);
|
||||
self.0.push(below.clone().pack(), styles);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -479,7 +478,7 @@ impl<'a> ParBuilder<'a> {
|
||||
|| content.is::<HNode>()
|
||||
|| content.is::<LinebreakNode>()
|
||||
|| content.is::<SmartQuoteNode>()
|
||||
|| content.to::<FormulaNode>().map_or(false, |node| !node.block)
|
||||
|| content.to::<FormulaNode>().map_or(false, |node| !node.block())
|
||||
|| content.is::<BoxNode>()
|
||||
{
|
||||
self.0.push(content.clone(), styles);
|
||||
@ -491,14 +490,14 @@ impl<'a> ParBuilder<'a> {
|
||||
|
||||
fn finish(self) -> (Content, StyleChain<'a>) {
|
||||
let (children, shared) = self.0.finish();
|
||||
(ParNode(children).pack(), shared)
|
||||
(ParNode::new(children.to_vec()).pack(), shared)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts list / enum items, spaces, paragraph breaks.
|
||||
struct ListBuilder<'a> {
|
||||
/// The list items collected so far.
|
||||
items: StyleVecBuilder<'a, ListItem>,
|
||||
items: StyleVecBuilder<'a, Content>,
|
||||
/// Whether the list contains no paragraph breaks.
|
||||
tight: bool,
|
||||
/// Trailing content for which it is unclear whether it is part of the list.
|
||||
@ -514,14 +513,18 @@ impl<'a> ListBuilder<'a> {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(item) = content.to::<ListItem>() {
|
||||
if self.items.items().next().map_or(true, |first| {
|
||||
std::mem::discriminant(item) == std::mem::discriminant(first)
|
||||
}) {
|
||||
self.items.push(item.clone(), styles);
|
||||
self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
|
||||
return true;
|
||||
}
|
||||
if (content.is::<ListItem>()
|
||||
|| content.is::<EnumItem>()
|
||||
|| content.is::<TermItem>())
|
||||
&& self
|
||||
.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>());
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
@ -530,31 +533,48 @@ impl<'a> ListBuilder<'a> {
|
||||
fn finish(self) -> (Content, StyleChain<'a>) {
|
||||
let (items, shared) = self.items.finish();
|
||||
let item = items.items().next().unwrap();
|
||||
let output = match item {
|
||||
ListItem::List(_) => ListNode {
|
||||
tight: self.tight,
|
||||
items: items.map(|item| match item {
|
||||
ListItem::List(item) => item.clone(),
|
||||
_ => panic!("wrong list item"),
|
||||
}),
|
||||
}
|
||||
.pack(),
|
||||
ListItem::Enum(..) => EnumNode {
|
||||
tight: self.tight,
|
||||
items: items.map(|item| match item {
|
||||
ListItem::Enum(number, body) => (*number, body.clone()),
|
||||
_ => panic!("wrong list item"),
|
||||
}),
|
||||
}
|
||||
.pack(),
|
||||
ListItem::Term(_) => TermsNode {
|
||||
tight: self.tight,
|
||||
items: items.map(|item| match item {
|
||||
ListItem::Term(item) => item.clone(),
|
||||
_ => panic!("wrong list item"),
|
||||
}),
|
||||
}
|
||||
.pack(),
|
||||
let output = if item.is::<ListItem>() {
|
||||
ListNode::new(
|
||||
items
|
||||
.iter()
|
||||
.map(|(item, map)| {
|
||||
let item = item.to::<ListItem>().unwrap();
|
||||
ListItem::new(item.body().styled_with_map(map.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.with_tight(self.tight)
|
||||
.pack()
|
||||
} else if item.is::<EnumItem>() {
|
||||
EnumNode::new(
|
||||
items
|
||||
.iter()
|
||||
.map(|(item, map)| {
|
||||
let item = item.to::<EnumItem>().unwrap();
|
||||
EnumItem::new(item.body().styled_with_map(map.clone()))
|
||||
.with_number(item.number())
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.with_tight(self.tight)
|
||||
.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)
|
||||
}
|
||||
@ -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 {}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Padding
|
||||
/// Add spacing around content.
|
||||
///
|
||||
/// The `pad` function adds spacing around content. The spacing can be specified
|
||||
@ -17,21 +16,6 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// ## 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)
|
||||
/// The horizontal padding. Both `left` and `right` take precedence over this.
|
||||
///
|
||||
@ -41,20 +25,37 @@ use crate::prelude::*;
|
||||
/// - rest: `Rel<Length>` (named)
|
||||
/// The padding for all sides. All other parameters take precedence over this.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Padding
|
||||
/// Category: layout
|
||||
#[node(Construct, Layout)]
|
||||
pub struct PadNode {
|
||||
/// The amount of padding.
|
||||
pub padding: Sides<Rel<Length>>,
|
||||
/// The content whose sides to pad.
|
||||
/// The content to pad at the sides.
|
||||
#[positional]
|
||||
#[required]
|
||||
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 PadNode {
|
||||
impl Construct for PadNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let all = args.named("rest")?.or(args.find()?);
|
||||
let x = args.named("x")?;
|
||||
@ -64,8 +65,12 @@ impl PadNode {
|
||||
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 body = args.expect::<Content>("body")?;
|
||||
let padding = Sides::new(left, top, right, bottom);
|
||||
Ok(Self { padding, body }.pack())
|
||||
Ok(Self::new(body)
|
||||
.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![];
|
||||
|
||||
// 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 mut fragment = self.body.layout(vt, styles, pod)?;
|
||||
let mut fragment = self.body().layout(vt, styles, pod)?;
|
||||
|
||||
for frame in &mut fragment {
|
||||
// Apply the padding inversely such that the grown size padded
|
||||
|
@ -3,7 +3,6 @@ use std::str::FromStr;
|
||||
use super::ColumnsNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Page
|
||||
/// Layouts its child onto one or multiple pages.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ## 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)
|
||||
/// A standard paper size to set width and height. When this is not specified,
|
||||
/// Typst defaults to `{"a4"}` paper.
|
||||
@ -33,15 +25,25 @@ use crate::prelude::*;
|
||||
/// There you go, US friends!
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable]
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct PageNode(pub Content);
|
||||
|
||||
/// Display: Page
|
||||
/// Category: layout
|
||||
#[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.
|
||||
///
|
||||
/// ```example
|
||||
@ -54,8 +56,10 @@ impl PageNode {
|
||||
/// box(square(width: 1cm))
|
||||
/// }
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width().into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Smart::Custom(Paper::A4.width().into()))]
|
||||
pub width: Smart<Length>,
|
||||
|
||||
/// The height of the page.
|
||||
///
|
||||
@ -63,8 +67,10 @@ impl PageNode {
|
||||
/// by inserting a [page break]($func/pagebreak). Most examples throughout
|
||||
/// this documentation use `{auto}` for the height of the page to
|
||||
/// dynamically grow and shrink to fit their content.
|
||||
#[property(resolve)]
|
||||
pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height().into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Smart::Custom(Paper::A4.height().into()))]
|
||||
pub height: Smart<Length>,
|
||||
|
||||
/// Whether the page is flipped into landscape orientation.
|
||||
///
|
||||
@ -84,7 +90,9 @@ impl PageNode {
|
||||
/// New York, NY 10001 \
|
||||
/// +1 555 555 5555
|
||||
/// ```
|
||||
pub const FLIPPED: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub flipped: bool,
|
||||
|
||||
/// The page's margins.
|
||||
///
|
||||
@ -114,8 +122,10 @@ impl PageNode {
|
||||
/// fill: aqua,
|
||||
/// )
|
||||
/// ```
|
||||
#[property(fold)]
|
||||
pub const MARGIN: Sides<Option<Smart<Rel<Length>>>> = Sides::splat(Smart::Auto);
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub margin: Sides<Option<Smart<Rel<Length>>>>,
|
||||
|
||||
/// How many columns the page has.
|
||||
///
|
||||
@ -131,7 +141,9 @@ impl PageNode {
|
||||
/// emissions and mitigate the impacts
|
||||
/// 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.
|
||||
///
|
||||
@ -145,7 +157,9 @@ impl PageNode {
|
||||
/// #set text(fill: rgb("fdfdfd"))
|
||||
/// *Dark mode enabled.*
|
||||
/// ```
|
||||
pub const FILL: Option<Paint> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// The page's header.
|
||||
///
|
||||
@ -166,8 +180,9 @@ impl PageNode {
|
||||
///
|
||||
/// #lorem(18)
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const HEADER: Option<Marginal> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub header: Option<Marginal>,
|
||||
|
||||
/// The page's footer.
|
||||
///
|
||||
@ -190,8 +205,9 @@ impl PageNode {
|
||||
///
|
||||
/// #lorem(18)
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const FOOTER: Option<Marginal> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub footer: Option<Marginal>,
|
||||
|
||||
/// Content in the page's background.
|
||||
///
|
||||
@ -211,8 +227,9 @@ impl PageNode {
|
||||
/// In the year 2023, we plan to take over the world
|
||||
/// (of typesetting).
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const BACKGROUND: Option<Marginal> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub background: Option<Marginal>,
|
||||
|
||||
/// Content in the page's foreground.
|
||||
///
|
||||
@ -228,26 +245,9 @@ impl PageNode {
|
||||
/// "Weak Reject" because they did
|
||||
/// not understand our approach...
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const FOREGROUND: Option<Marginal> = None;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default]
|
||||
pub foreground: Option<Marginal>,
|
||||
}
|
||||
|
||||
impl PageNode {
|
||||
@ -276,26 +276,22 @@ impl PageNode {
|
||||
let default = Rel::from(0.1190 * min);
|
||||
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.
|
||||
let columns = styles.get(Self::COLUMNS);
|
||||
if columns.get() > 1 {
|
||||
child = ColumnsNode { count: columns, body: self.0.clone() }.pack();
|
||||
child = ColumnsNode::new(columns, child).pack();
|
||||
}
|
||||
|
||||
// Realize margins.
|
||||
child = child.padded(padding);
|
||||
|
||||
// Realize background fill.
|
||||
if let Some(fill) = styles.get(Self::FILL) {
|
||||
child = child.filled(fill);
|
||||
}
|
||||
|
||||
// Layout the child.
|
||||
let regions = Regions::repeat(size, size.map(Abs::is_finite));
|
||||
let mut fragment = child.layout(vt, styles, regions)?;
|
||||
|
||||
let fill = styles.get(Self::FILL);
|
||||
let header = styles.get(Self::HEADER);
|
||||
let footer = styles.get(Self::FOOTER);
|
||||
let foreground = styles.get(Self::FOREGROUND);
|
||||
@ -303,17 +299,21 @@ impl PageNode {
|
||||
|
||||
// Realize overlays.
|
||||
for frame in &mut fragment {
|
||||
if let Some(fill) = fill {
|
||||
frame.fill(fill);
|
||||
}
|
||||
|
||||
let size = frame.size();
|
||||
let pad = padding.resolve(styles).relative_to(size);
|
||||
let pw = size.x - pad.left - pad.right;
|
||||
let py = size.y - pad.bottom;
|
||||
for (marginal, pos, area) in [
|
||||
(header, Point::with_x(pad.left), Size::new(pw, pad.top)),
|
||||
(footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)),
|
||||
(foreground, Point::zero(), size),
|
||||
(background, Point::zero(), size),
|
||||
(&header, Point::with_x(pad.left), Size::new(pw, pad.top)),
|
||||
(&footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)),
|
||||
(&foreground, 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 content = marginal.resolve(vt, page)?;
|
||||
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.
|
||||
///
|
||||
/// Must not be used inside any containers.
|
||||
@ -355,26 +346,15 @@ impl Debug for PageNode {
|
||||
/// In 1984, the first ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - weak: `bool` (named)
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Display: Page Break
|
||||
/// Category: layout
|
||||
#[node]
|
||||
impl PagebreakNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let weak = args.named("weak")?.unwrap_or(false);
|
||||
Ok(Self { weak }.pack())
|
||||
}
|
||||
pub struct PagebreakNode {
|
||||
/// If `{true}`, the page break is skipped if the current page is already
|
||||
/// empty.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
pub weak: bool,
|
||||
}
|
||||
|
||||
/// A header, footer, foreground or background definition.
|
||||
@ -399,12 +379,19 @@ impl Marginal {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Marginal,
|
||||
v: Content => Self::Content(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.
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub struct Paper {
|
||||
@ -450,7 +437,7 @@ macro_rules! papers {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Paper,
|
||||
$(
|
||||
/// Produces a paper of the respective size.
|
||||
|
@ -2,7 +2,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||
use unicode_script::{Script, UnicodeScript};
|
||||
use xi_unicode::LineBreakIterator;
|
||||
|
||||
use typst::model::Key;
|
||||
use typst::model::{Key, StyledNode};
|
||||
|
||||
use super::{BoxNode, HNode, Sizing, Spacing};
|
||||
use crate::layout::AlignNode;
|
||||
@ -12,7 +12,6 @@ use crate::text::{
|
||||
shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode,
|
||||
};
|
||||
|
||||
/// # Paragraph
|
||||
/// Arrange text, spacing and inline-level nodes into a paragraph.
|
||||
///
|
||||
/// Although this function is primarily used in set rules to affect paragraph
|
||||
@ -40,15 +39,14 @@ use crate::text::{
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The contents of the paragraph.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable]
|
||||
#[derive(Hash)]
|
||||
pub struct ParNode(pub StyleVec<Content>);
|
||||
/// Display: Paragraph
|
||||
/// Category: layout
|
||||
#[node(Construct)]
|
||||
pub struct ParNode {
|
||||
/// The paragraph's children.
|
||||
#[variadic]
|
||||
pub children: Vec<Content>,
|
||||
|
||||
#[node]
|
||||
impl ParNode {
|
||||
/// The indent the first line of a consecutive paragraph should have.
|
||||
///
|
||||
/// 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
|
||||
/// [paragraph spacing]($func/block.spacing) off when using this property
|
||||
/// (e.g. using `[#show par: set block(spacing: 0pt)]`).
|
||||
#[property(resolve)]
|
||||
pub const INDENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub indent: Length,
|
||||
|
||||
/// The spacing between lines.
|
||||
///
|
||||
/// The default value is `{0.65em}`.
|
||||
#[property(resolve)]
|
||||
pub const LEADING: Length = Em::new(0.65).into();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Em::new(0.65).into())]
|
||||
pub leading: Length,
|
||||
|
||||
/// 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
|
||||
/// the placement of the last line except if it ends with a [justified line
|
||||
/// break]($func/linebreak.justify).
|
||||
pub const JUSTIFY: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub justify: bool,
|
||||
|
||||
/// How to determine line breaks.
|
||||
///
|
||||
@ -100,16 +104,20 @@ impl ParNode {
|
||||
/// very aesthetic example is one
|
||||
/// 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> {
|
||||
// The paragraph constructor is special: It doesn't create a paragraph
|
||||
// node. Instead, it just ensures that the passed content lives in a
|
||||
// separate paragraph and styles it.
|
||||
Ok(Content::sequence(vec![
|
||||
ParbreakNode.pack(),
|
||||
ParbreakNode::new().pack(),
|
||||
args.expect("body")?,
|
||||
ParbreakNode.pack(),
|
||||
ParbreakNode::new().pack(),
|
||||
]))
|
||||
}
|
||||
}
|
||||
@ -136,14 +144,15 @@ impl ParNode {
|
||||
expand: bool,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut vt = Vt { world, provider, introspector };
|
||||
let children = par.children();
|
||||
|
||||
// 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
|
||||
// representation on which we can do line breaking without layouting
|
||||
// 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.
|
||||
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.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct HorizontalAlign(pub GenAlign);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
HorizontalAlign,
|
||||
align: GenAlign => match align.axis() {
|
||||
Axis::X => Self(align),
|
||||
@ -201,7 +203,7 @@ pub enum Linebreaks {
|
||||
Optimized,
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Linebreaks,
|
||||
/// Determine the line breaks in a simple first-fit style.
|
||||
"simple" => Self::Simple,
|
||||
@ -212,7 +214,13 @@ castable! {
|
||||
"optimized" => Self::Optimized,
|
||||
}
|
||||
|
||||
/// # Paragraph Break
|
||||
cast_to_value! {
|
||||
v: Linebreaks => Value::from(match v {
|
||||
Linebreaks::Simple => "simple",
|
||||
Linebreaks::Optimized => "optimized",
|
||||
})
|
||||
}
|
||||
|
||||
/// A paragraph break.
|
||||
///
|
||||
/// 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
|
||||
/// markup to create a paragraph break.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Unlabellable)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ParbreakNode;
|
||||
|
||||
#[node]
|
||||
impl ParbreakNode {
|
||||
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self.pack())
|
||||
}
|
||||
}
|
||||
/// Display: Paragraph Break
|
||||
/// Category: layout
|
||||
#[node(Unlabellable)]
|
||||
pub struct ParbreakNode {}
|
||||
|
||||
impl Unlabellable for ParbreakNode {}
|
||||
|
||||
@ -343,7 +342,7 @@ impl Segment<'_> {
|
||||
match *self {
|
||||
Self::Text(len) => len,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@ -485,21 +484,20 @@ impl<'a> Line<'a> {
|
||||
/// Collect all text of the paragraph into one string. This also performs
|
||||
/// string-level preprocessing like case transformations.
|
||||
fn collect<'a>(
|
||||
par: &'a ParNode,
|
||||
children: &'a [Content],
|
||||
styles: &'a StyleChain<'a>,
|
||||
consecutive: bool,
|
||||
) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) {
|
||||
) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> {
|
||||
let mut full = String::new();
|
||||
let mut quoter = Quoter::new();
|
||||
let mut segments = vec![];
|
||||
let mut iter = par.0.iter().peekable();
|
||||
let mut iter = children.iter().peekable();
|
||||
|
||||
if consecutive {
|
||||
let indent = styles.get(ParNode::INDENT);
|
||||
if !indent.is_zero()
|
||||
&& par
|
||||
.0
|
||||
.items()
|
||||
&& children
|
||||
.iter()
|
||||
.find_map(|child| {
|
||||
if child.with::<dyn Behave>().map_or(false, |behaved| {
|
||||
behaved.behaviour() == Behaviour::Ignorant
|
||||
@ -518,24 +516,30 @@ fn collect<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((child, map)) = iter.next() {
|
||||
let styles = styles.chain(map);
|
||||
while let Some(mut child) = iter.next() {
|
||||
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>() {
|
||||
full.push(' ');
|
||||
Segment::Text(1)
|
||||
} else if let Some(node) = child.to::<TextNode>() {
|
||||
let prev = full.len();
|
||||
if let Some(case) = styles.get(TextNode::CASE) {
|
||||
full.push_str(&case.apply(&node.0));
|
||||
full.push_str(&case.apply(&node.text()));
|
||||
} else {
|
||||
full.push_str(&node.0);
|
||||
full.push_str(&node.text());
|
||||
}
|
||||
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);
|
||||
Segment::Spacing(node.amount)
|
||||
Segment::Spacing(node.amount())
|
||||
} 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);
|
||||
Segment::Text(c.len_utf8())
|
||||
} else if let Some(node) = child.to::<SmartQuoteNode>() {
|
||||
@ -544,9 +548,9 @@ fn collect<'a>(
|
||||
let lang = styles.get(TextNode::LANG);
|
||||
let region = styles.get(TextNode::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>() {
|
||||
node.0.chars().next()
|
||||
node.text().chars().next()
|
||||
} else if child.is::<SmartQuoteNode>() {
|
||||
Some('"')
|
||||
} else if child.is::<SpaceNode>() || child.is::<HNode>() {
|
||||
@ -556,23 +560,25 @@ fn collect<'a>(
|
||||
}
|
||||
});
|
||||
|
||||
full.push_str(quoter.quote("es, node.double, peeked));
|
||||
full.push_str(quoter.quote("es, node.double(), peeked));
|
||||
} else {
|
||||
full.push(if node.double { '"' } else { '\'' });
|
||||
full.push(if node.double() { '"' } else { '\'' });
|
||||
}
|
||||
Segment::Text(full.len() - prev)
|
||||
} else if let Some(node) = child.to::<FormulaNode>() {
|
||||
full.push(NODE_REPLACE);
|
||||
Segment::Formula(node)
|
||||
} else if let Some(node) = child.to::<BoxNode>() {
|
||||
full.push(if node.width.is_fractional() {
|
||||
full.push(if node.width().is_fractional() {
|
||||
SPACING_REPLACE
|
||||
} else {
|
||||
NODE_REPLACE
|
||||
});
|
||||
Segment::Box(node)
|
||||
} else if let Some(span) = child.span() {
|
||||
bail!(span, "unexpected document child");
|
||||
} else {
|
||||
panic!("unexpected par child: {child:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(last) = full.chars().last() {
|
||||
@ -591,14 +597,14 @@ fn collect<'a>(
|
||||
segments.push((segment, styles));
|
||||
}
|
||||
|
||||
(full, segments)
|
||||
Ok((full, segments))
|
||||
}
|
||||
|
||||
/// Prepare paragraph layout by shaping the whole paragraph and layouting all
|
||||
/// contained inline-level content.
|
||||
fn prepare<'a>(
|
||||
vt: &mut Vt,
|
||||
par: &'a ParNode,
|
||||
children: &'a [Content],
|
||||
text: &'a str,
|
||||
segments: Vec<(Segment<'a>, StyleChain<'a>)>,
|
||||
styles: StyleChain<'a>,
|
||||
@ -639,7 +645,7 @@ fn prepare<'a>(
|
||||
items.push(Item::Frame(frame));
|
||||
}
|
||||
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))));
|
||||
} else {
|
||||
let pod = Regions::one(region, Axes::splat(false));
|
||||
@ -657,9 +663,9 @@ fn prepare<'a>(
|
||||
bidi,
|
||||
items,
|
||||
styles,
|
||||
hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE),
|
||||
lang: shared_get(styles, &par.0, TextNode::LANG),
|
||||
align: styles.get(AlignNode::ALIGNS).x.resolve(styles),
|
||||
hyphenate: shared_get(styles, children, TextNode::HYPHENATE),
|
||||
lang: shared_get(styles, children, TextNode::LANG),
|
||||
align: styles.get(AlignNode::ALIGNMENT).x.resolve(styles),
|
||||
justify: styles.get(ParNode::JUSTIFY),
|
||||
})
|
||||
}
|
||||
@ -722,12 +728,13 @@ fn is_compatible(a: Script, b: Script) -> bool {
|
||||
/// paragraph.
|
||||
fn shared_get<'a, K: Key>(
|
||||
styles: StyleChain<'a>,
|
||||
children: &StyleVec<Content>,
|
||||
children: &[Content],
|
||||
key: K,
|
||||
) -> Option<K::Output<'a>> {
|
||||
) -> Option<K::Output> {
|
||||
children
|
||||
.styles()
|
||||
.all(|map| !map.contains(key))
|
||||
.iter()
|
||||
.filter_map(|child| child.to::<StyledNode>())
|
||||
.all(|node| !node.map().contains(key))
|
||||
.then(|| styles.get(key))
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Place
|
||||
/// Place content at an absolute position.
|
||||
///
|
||||
/// Placed content will not affect the position of other content. Place is
|
||||
@ -22,48 +21,41 @@ use crate::prelude::*;
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - alignment: `Axes<Option<GenAlign>>` (positional)
|
||||
/// Relative to which position in the parent container to place the content.
|
||||
///
|
||||
/// When an axis of the page is `{auto}` sized, all alignments relative to that
|
||||
/// axis will be ignored, instead, the item will be placed in the origin of the
|
||||
/// axis.
|
||||
///
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to place.
|
||||
///
|
||||
/// - dx: `Rel<Length>` (named)
|
||||
/// 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);
|
||||
/// Display: Place
|
||||
/// Category: layout
|
||||
#[node(Layout, Behave)]
|
||||
pub struct PlaceNode {
|
||||
/// Relative to which position in the parent container to place the content.
|
||||
///
|
||||
/// When an axis of the page is `{auto}` sized, all alignments relative to that
|
||||
/// axis will be ignored, instead, the item will be placed in the origin of the
|
||||
/// axis.
|
||||
#[positional]
|
||||
#[default(Axes::with_x(Some(GenAlign::Start)))]
|
||||
pub alignment: Axes<Option<GenAlign>>,
|
||||
|
||||
#[node]
|
||||
impl PlaceNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start)));
|
||||
let dx = args.named("dx")?.unwrap_or_default();
|
||||
let dy = args.named("dy")?.unwrap_or_default();
|
||||
let body = args.expect::<Content>("body")?;
|
||||
let out_of_flow = aligns.y.is_some();
|
||||
Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns), out_of_flow).pack())
|
||||
}
|
||||
/// The content to place.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
/// 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]
|
||||
/// }
|
||||
/// ```
|
||||
#[named]
|
||||
#[default]
|
||||
pub dx: Rel<Length>,
|
||||
|
||||
/// The vertical displacement of the placed content.
|
||||
#[named]
|
||||
#[default]
|
||||
pub dy: Rel<Length>,
|
||||
}
|
||||
|
||||
impl Layout for PlaceNode {
|
||||
@ -83,7 +75,12 @@ impl Layout for PlaceNode {
|
||||
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
|
||||
// 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
|
||||
/// position.
|
||||
pub fn out_of_flow(&self) -> bool {
|
||||
self.1
|
||||
self.alignment().y.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ use crate::prelude::*;
|
||||
|
||||
use super::AlignNode;
|
||||
|
||||
/// # Repeat
|
||||
/// Repeats content to the available space.
|
||||
///
|
||||
/// This can be useful when implementing a custom index, reference, or outline.
|
||||
@ -22,22 +21,14 @@ use super::AlignNode;
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to repeat.
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Repeat
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct RepeatNode {
|
||||
/// The content to repeat.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl Layout for RepeatNode {
|
||||
@ -48,8 +39,8 @@ impl Layout for RepeatNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.size, Axes::new(false, false));
|
||||
let piece = self.0.layout(vt, styles, pod)?.into_frame();
|
||||
let align = styles.get(AlignNode::ALIGNS).x.resolve(styles);
|
||||
let piece = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles);
|
||||
|
||||
let fill = regions.size.x;
|
||||
let width = piece.width();
|
||||
|
@ -2,7 +2,6 @@ use std::cmp::Ordering;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Spacing (H)
|
||||
/// Insert horizontal spacing into a paragraph.
|
||||
///
|
||||
/// 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
|
||||
/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - amount: `Spacing` (positional, required)
|
||||
/// How much spacing to insert.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Spacing (H)
|
||||
/// Category: layout
|
||||
#[node(Behave)]
|
||||
pub struct HNode {
|
||||
/// The amount of horizontal spacing.
|
||||
/// How much spacing to insert.
|
||||
#[positional]
|
||||
#[required]
|
||||
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,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
if self.amount.is_fractional() {
|
||||
if self.amount().is_fractional() {
|
||||
Behaviour::Destructive
|
||||
} else if self.weak {
|
||||
} else if self.weak() {
|
||||
Behaviour::Weak(1)
|
||||
} else {
|
||||
Behaviour::Ignorant
|
||||
@ -86,11 +60,10 @@ impl Behave for HNode {
|
||||
|
||||
fn larger(&self, prev: &Content) -> bool {
|
||||
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.
|
||||
///
|
||||
/// The spacing can be absolute, relative, or fractional. In the last case,
|
||||
@ -130,20 +103,24 @@ impl Behave for HNode {
|
||||
/// #v(4pt, weak: true)
|
||||
/// The proof is simple:
|
||||
/// ```
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Behave)]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
|
||||
///
|
||||
/// Display: Spacing (V)
|
||||
/// Category: layout
|
||||
#[node(Construct, Behave)]
|
||||
pub struct VNode {
|
||||
/// The amount of vertical spacing.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub amount: Spacing,
|
||||
|
||||
/// The node's weakness level, see also [`Behaviour`].
|
||||
pub weakness: u8,
|
||||
#[named]
|
||||
#[skip]
|
||||
#[default]
|
||||
pub weakness: usize,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl VNode {
|
||||
impl Construct for VNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let amount = args.expect("spacing")?;
|
||||
let node = if args.named("weak")?.unwrap_or(false) {
|
||||
@ -158,36 +135,36 @@ impl VNode {
|
||||
impl VNode {
|
||||
/// Normal strong spacing.
|
||||
pub fn strong(amount: Spacing) -> Self {
|
||||
Self { amount, weakness: 0 }
|
||||
Self::new(amount).with_weakness(0)
|
||||
}
|
||||
|
||||
/// User-created weak spacing.
|
||||
pub fn weak(amount: Spacing) -> Self {
|
||||
Self { amount, weakness: 1 }
|
||||
Self::new(amount).with_weakness(1)
|
||||
}
|
||||
|
||||
/// Weak spacing with list attach weakness.
|
||||
pub fn list_attach(amount: Spacing) -> Self {
|
||||
Self { amount, weakness: 2 }
|
||||
Self::new(amount).with_weakness(2)
|
||||
}
|
||||
|
||||
/// Weak spacing with BlockNode::ABOVE/BELOW weakness.
|
||||
pub fn block_around(amount: Spacing) -> Self {
|
||||
Self { amount, weakness: 3 }
|
||||
Self::new(amount).with_weakness(3)
|
||||
}
|
||||
|
||||
/// Weak spacing with BlockNode::SPACING weakness.
|
||||
pub fn block_spacing(amount: Spacing) -> Self {
|
||||
Self { amount, weakness: 4 }
|
||||
Self::new(amount).with_weakness(4)
|
||||
}
|
||||
}
|
||||
|
||||
impl Behave for VNode {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
if self.amount.is_fractional() {
|
||||
if self.amount().is_fractional() {
|
||||
Behaviour::Destructive
|
||||
} else if self.weakness > 0 {
|
||||
Behaviour::Weak(self.weakness)
|
||||
} else if self.weakness() > 0 {
|
||||
Behaviour::Weak(self.weakness())
|
||||
} else {
|
||||
Behaviour::Ignorant
|
||||
}
|
||||
@ -195,10 +172,15 @@ impl Behave for VNode {
|
||||
|
||||
fn larger(&self, prev: &Content) -> bool {
|
||||
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.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Spacing {
|
||||
@ -214,22 +196,6 @@ impl Spacing {
|
||||
pub fn is_fractional(self) -> bool {
|
||||
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 {
|
||||
@ -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 {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
@ -254,8 +226,23 @@ impl PartialOrd for Spacing {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Spacing,
|
||||
v: Rel<Length> => Self::Rel(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),
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ use typst::model::StyledNode;
|
||||
use super::{AlignNode, Spacing};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Stack
|
||||
/// Arrange content and spacing horizontally or vertically.
|
||||
///
|
||||
/// The stack places a list of items along an axis, with optional spacing
|
||||
@ -19,45 +18,28 @@ use crate::prelude::*;
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - items: `StackChild` (positional, variadic)
|
||||
/// The items to stack along an axis.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Stack
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct StackNode {
|
||||
/// The stacking direction.
|
||||
pub dir: Dir,
|
||||
/// The spacing between non-spacing children.
|
||||
pub spacing: Option<Spacing>,
|
||||
/// The children to be stacked.
|
||||
/// The childfren to stack along the axis.
|
||||
#[variadic]
|
||||
pub children: Vec<StackChild>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl StackNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self {
|
||||
dir: args.named("dir")?.unwrap_or(Dir::TTB),
|
||||
spacing: args.named("spacing")?,
|
||||
children: args.all()?,
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
/// 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.
|
||||
#[named]
|
||||
#[default(Dir::TTB)]
|
||||
pub dir: Dir,
|
||||
|
||||
/// Spacing to insert between items where no explicit spacing was provided.
|
||||
#[named]
|
||||
#[default]
|
||||
pub spacing: Option<Spacing>,
|
||||
}
|
||||
|
||||
impl Layout for StackNode {
|
||||
@ -67,15 +49,16 @@ impl Layout for StackNode {
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> 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.
|
||||
let spacing = self.spacing();
|
||||
let mut deferred = None;
|
||||
|
||||
for child in &self.children {
|
||||
for child in self.children() {
|
||||
match child {
|
||||
StackChild::Spacing(kind) => {
|
||||
layouter.layout_spacing(*kind);
|
||||
layouter.layout_spacing(kind);
|
||||
deferred = None;
|
||||
}
|
||||
StackChild::Block(block) => {
|
||||
@ -83,8 +66,8 @@ impl Layout for StackNode {
|
||||
layouter.layout_spacing(kind);
|
||||
}
|
||||
|
||||
layouter.layout_block(vt, block, styles)?;
|
||||
deferred = self.spacing;
|
||||
layouter.layout_block(vt, &block, styles)?;
|
||||
deferred = spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,10 +94,17 @@ impl Debug for StackChild {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
StackChild,
|
||||
spacing: Spacing => Self::Spacing(spacing),
|
||||
content: Content => Self::Block(content),
|
||||
v: Spacing => Self::Spacing(v),
|
||||
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.
|
||||
@ -212,9 +202,9 @@ impl<'a> StackLayouter<'a> {
|
||||
// Block-axis alignment of the `AlignNode` is respected
|
||||
// by the stack node.
|
||||
let aligns = if let Some(styled) = block.to::<StyledNode>() {
|
||||
styles.chain(&styled.map).get(AlignNode::ALIGNS)
|
||||
styles.chain(&styled.map()).get(AlignNode::ALIGNMENT)
|
||||
} else {
|
||||
styles.get(AlignNode::ALIGNS)
|
||||
styles.get(AlignNode::ALIGNMENT)
|
||||
};
|
||||
|
||||
let aligns = aligns.resolve(styles);
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::layout::{AlignNode, GridLayouter, Sizing, TrackSizings};
|
||||
use crate::layout::{AlignNode, GridLayouter, TrackSizings};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Table
|
||||
/// A table of items.
|
||||
///
|
||||
/// Tables are used to arrange content in cells. Cells can contain arbitrary
|
||||
@ -31,47 +30,46 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// ## 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)
|
||||
/// Defines the gaps between rows & columns.
|
||||
/// See the [grid documentation]($func/grid) for more information on gutters.
|
||||
///
|
||||
/// - column-gutter: `TrackSizings` (named)
|
||||
/// Defines the gaps between columns. Takes precedence over `gutter`.
|
||||
/// See the [grid documentation]($func/grid) for more information on gutters.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Table
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct TableNode {
|
||||
/// Defines sizing for content rows and columns.
|
||||
pub tracks: Axes<Vec<Sizing>>,
|
||||
/// Defines sizing of gutter rows and columns between content.
|
||||
pub gutter: Axes<Vec<Sizing>>,
|
||||
/// The content to be arranged in the table.
|
||||
/// The contents of the table cells.
|
||||
#[variadic]
|
||||
pub cells: Vec<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl TableNode {
|
||||
/// Defines the column sizes.
|
||||
/// 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.
|
||||
///
|
||||
/// 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 €],
|
||||
/// )
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const FILL: Celled<Option<Paint>> = Celled::Value(None);
|
||||
#[settable]
|
||||
#[default]
|
||||
pub fill: Celled<Option<Paint>>,
|
||||
|
||||
/// How to align the cell's content.
|
||||
///
|
||||
/// 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,
|
||||
/// starting at zero. If set to `{auto}`, the outer alignment is used.
|
||||
#[property(referenced)]
|
||||
pub const ALIGN: Celled<Smart<Axes<Option<GenAlign>>>> = Celled::Value(Smart::Auto);
|
||||
#[settable]
|
||||
#[default]
|
||||
pub align: Celled<Smart<Axes<Option<GenAlign>>>>,
|
||||
|
||||
/// How to stroke the cells.
|
||||
///
|
||||
/// This can be a color, a stroke width, both, or `{none}` to disable
|
||||
/// the stroke.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(PartialStroke::default()))]
|
||||
pub stroke: Option<PartialStroke>,
|
||||
|
||||
/// How much to pad the cells's content.
|
||||
///
|
||||
/// The default value is `{5pt}`.
|
||||
pub const INSET: Rel<Length> = Abs::pt(5.0).into();
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(Abs::pt(5.0).into())]
|
||||
pub inset: Rel<Length>,
|
||||
}
|
||||
|
||||
impl Layout for TableNode {
|
||||
@ -156,11 +131,12 @@ impl Layout for TableNode {
|
||||
let inset = styles.get(Self::INSET);
|
||||
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
|
||||
.cells
|
||||
.iter()
|
||||
.cloned()
|
||||
.cells()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, child)| {
|
||||
let mut child = child.padded(Sides::splat(inset));
|
||||
@ -168,7 +144,7 @@ impl Layout for TableNode {
|
||||
let x = i % cols;
|
||||
let y = i / cols;
|
||||
if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
|
||||
child = child.styled(AlignNode::ALIGNS, alignment)
|
||||
child = child.styled(AlignNode::ALIGNMENT, alignment)
|
||||
}
|
||||
|
||||
Ok(child)
|
||||
@ -181,8 +157,8 @@ impl Layout for TableNode {
|
||||
// Prepare grid layout by unifying content and gutter tracks.
|
||||
let layouter = GridLayouter::new(
|
||||
vt,
|
||||
self.tracks.as_deref(),
|
||||
self.gutter.as_deref(),
|
||||
tracks.as_deref(),
|
||||
gutter.as_deref(),
|
||||
&cells,
|
||||
regions,
|
||||
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> {
|
||||
fn is(value: &Value) -> bool {
|
||||
matches!(value, Value::Func(_)) || T::is(value)
|
||||
@ -286,3 +268,12 @@ impl<T: Cast> Cast for Celled<T> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::text::{SpaceNode, TextNode};
|
||||
|
||||
/// # Term List
|
||||
/// A list of terms and their descriptions.
|
||||
///
|
||||
/// Displays a sequence of terms and their descriptions vertically. When the
|
||||
@ -20,55 +19,49 @@ use crate::text::{SpaceNode, TextNode};
|
||||
/// between two adjacent letters.
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - items: `Content` (positional, variadic)
|
||||
/// The term list's children.
|
||||
///
|
||||
/// When using the term list syntax, adjacent items are automatically
|
||||
/// collected into term lists, even through constructs like for loops.
|
||||
///
|
||||
/// ```example
|
||||
/// #for year, product in (
|
||||
/// "1978": "TeX",
|
||||
/// "1984": "LaTeX",
|
||||
/// "2019": "Typst",
|
||||
/// ) [/ #product: Born in #year.]
|
||||
/// ```
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Term List
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct TermsNode {
|
||||
/// If true, the items are separated by leading instead of list spacing.
|
||||
pub tight: bool,
|
||||
/// The individual bulleted or numbered items.
|
||||
pub items: StyleVec<TermItem>,
|
||||
}
|
||||
/// The term list's children.
|
||||
///
|
||||
/// When using the term list syntax, adjacent items are automatically
|
||||
/// collected into term lists, even through constructs like for loops.
|
||||
///
|
||||
/// ```example
|
||||
/// #for year, product in (
|
||||
/// "1978": "TeX",
|
||||
/// "1984": "LaTeX",
|
||||
/// "2019": "Typst",
|
||||
/// ) [/ #product: Born in #year.]
|
||||
/// ```
|
||||
#[variadic]
|
||||
pub 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.
|
||||
#[property(resolve)]
|
||||
pub const INDENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub indent: Length,
|
||||
|
||||
/// The hanging indent of the description.
|
||||
///
|
||||
@ -77,31 +70,17 @@ impl TermsNode {
|
||||
/// / Term: This term list does not
|
||||
/// make use of hanging indents.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const HANGING_INDENT: Length = Em::new(1.0).into();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Em::new(1.0).into())]
|
||||
pub hanging_indent: Length,
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) term list.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
pub const SPACING: Smart<Spacing> = Smart::Auto;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default]
|
||||
pub spacing: Smart<Spacing>,
|
||||
}
|
||||
|
||||
impl Layout for TermsNode {
|
||||
@ -113,66 +92,63 @@ impl Layout for TermsNode {
|
||||
) -> SourceResult<Fragment> {
|
||||
let indent = styles.get(Self::INDENT);
|
||||
let body_indent = styles.get(Self::HANGING_INDENT);
|
||||
let gutter = if self.tight {
|
||||
let gutter = if self.tight() {
|
||||
styles.get(ParNode::LEADING).into()
|
||||
} else {
|
||||
styles
|
||||
.get(Self::SPACING)
|
||||
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount)
|
||||
.unwrap_or_else(|| styles.get(BlockNode::BELOW).amount())
|
||||
};
|
||||
|
||||
let mut cells = vec![];
|
||||
for (item, map) in self.items.iter() {
|
||||
for item in self.items() {
|
||||
let body = Content::sequence(vec![
|
||||
HNode { amount: (-body_indent).into(), weak: false }.pack(),
|
||||
(item.term.clone() + TextNode::packed(':')).strong(),
|
||||
SpaceNode.pack(),
|
||||
item.description.clone(),
|
||||
HNode::new((-body_indent).into()).pack(),
|
||||
(item.term() + TextNode::packed(':')).strong(),
|
||||
SpaceNode::new().pack(),
|
||||
item.description(),
|
||||
]);
|
||||
|
||||
cells.push(Content::empty());
|
||||
cells.push(body.styled_with_map(map.clone()));
|
||||
cells.push(body);
|
||||
}
|
||||
|
||||
GridNode {
|
||||
tracks: Axes::with_x(vec![
|
||||
Sizing::Rel((indent + body_indent).into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
gutter: Axes::with_y(vec![gutter.into()]),
|
||||
cells,
|
||||
}
|
||||
.layout(vt, styles, regions)
|
||||
let layouter = GridLayouter::new(
|
||||
vt,
|
||||
Axes::with_x(&[Sizing::Rel((indent + body_indent).into()), Sizing::Auto]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
&cells,
|
||||
regions,
|
||||
styles,
|
||||
);
|
||||
|
||||
Ok(layouter.layout()?.fragment)
|
||||
}
|
||||
}
|
||||
|
||||
/// A term list item.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
#[node]
|
||||
pub struct TermItem {
|
||||
/// The term described by the list item.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub term: Content,
|
||||
|
||||
/// The description of the term.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub description: Content,
|
||||
}
|
||||
|
||||
impl TermItem {
|
||||
/// Encode the item into a value.
|
||||
fn encode(&self) -> Value {
|
||||
Value::Array(array![
|
||||
Value::Content(self.term.clone()),
|
||||
Value::Content(self.description.clone()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
TermItem,
|
||||
array: Array => {
|
||||
let mut iter = array.into_iter();
|
||||
let (term, description) = match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(a), Some(b), None) => (a.cast()?, b.cast()?),
|
||||
_ => 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")?,
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ use typst::geom::Transform;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Move
|
||||
/// Move content without affecting layout.
|
||||
///
|
||||
/// The `move` function allows you to move content while the layout still 'sees'
|
||||
@ -22,39 +21,24 @@ use crate::prelude::*;
|
||||
/// ))
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to move.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Move
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
pub struct MoveNode {
|
||||
/// The offset by which to move the content.
|
||||
pub delta: Axes<Rel<Length>>,
|
||||
/// The content that should be moved.
|
||||
/// The content to move.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl MoveNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let dx = args.named("dx")?.unwrap_or_default();
|
||||
let dy = args.named("dy")?.unwrap_or_default();
|
||||
Ok(Self {
|
||||
delta: Axes::new(dx, dy),
|
||||
body: args.expect("body")?,
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
/// The horizontal displacement of the content.
|
||||
#[named]
|
||||
#[default]
|
||||
pub dx: Rel<Length>,
|
||||
|
||||
/// The vertical displacement of the content.
|
||||
#[named]
|
||||
#[default]
|
||||
pub dy: Rel<Length>,
|
||||
}
|
||||
|
||||
impl Layout for MoveNode {
|
||||
@ -65,15 +49,14 @@ impl Layout for MoveNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
|
||||
let delta = self.delta.resolve(styles);
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let delta = Axes::new(self.dx(), self.dy()).resolve(styles);
|
||||
let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s));
|
||||
frame.translate(delta.to_point());
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Rotate
|
||||
/// Rotate content with affecting layout.
|
||||
///
|
||||
/// Rotate an element by a given angle. The layout will act as if the element
|
||||
@ -89,31 +72,26 @@ impl Layout for MoveNode {
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to rotate.
|
||||
///
|
||||
/// - angle: `Angle` (named)
|
||||
/// The amount of rotation.
|
||||
///
|
||||
/// ```example
|
||||
/// #rotate(angle: -1.571rad)[Space!]
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// layout
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Rotate
|
||||
/// Category: layout
|
||||
#[node(Layout)]
|
||||
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,
|
||||
/// The content that should be rotated.
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl RotateNode {
|
||||
/// The content to rotate.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
/// The origin of the rotation.
|
||||
///
|
||||
/// 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: bottom + right, square()))
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default();
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self {
|
||||
angle: args.named_or_find("angle")?.unwrap_or_default(),
|
||||
body: args.expect("body")?,
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub origin: Axes<Option<GenAlign>>,
|
||||
}
|
||||
|
||||
impl Layout for RotateNode {
|
||||
@ -150,18 +122,17 @@ impl Layout for RotateNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
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 ts = Transform::translate(x, y)
|
||||
.pre_concat(Transform::rotate(self.angle))
|
||||
.pre_concat(Transform::rotate(self.angle()))
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
frame.transform(ts);
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Scale
|
||||
/// Scale content without affecting layout.
|
||||
///
|
||||
/// 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.]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to scale.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Scale
|
||||
/// Category: layout
|
||||
#[node(Construct, Layout)]
|
||||
pub struct ScaleNode {
|
||||
/// Scaling factor.
|
||||
pub factor: Axes<Ratio>,
|
||||
/// The content that should be scaled.
|
||||
/// The content to scale.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl ScaleNode {
|
||||
/// The horizontal scaling factor.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// By default, the origin is the center of the scaled element.
|
||||
@ -210,18 +176,18 @@ impl ScaleNode {
|
||||
/// A#box(scale(75%)[A])A \
|
||||
/// B#box(scale(75%, origin: bottom + left)[B])B
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub origin: Axes<Option<GenAlign>>,
|
||||
}
|
||||
|
||||
impl Construct for ScaleNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let all = args.find()?;
|
||||
let x = args.named("x")?.or(all).unwrap_or(Ratio::one());
|
||||
let y = args.named("y")?.or(all).unwrap_or(Ratio::one());
|
||||
Ok(Self {
|
||||
factor: Axes::new(x, y),
|
||||
body: args.expect("body")?,
|
||||
}
|
||||
.pack())
|
||||
Ok(Self::new(args.expect::<Content>("body")?).with_x(x).with_y(y).pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,11 +199,11 @@ impl Layout for ScaleNode {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
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 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));
|
||||
frame.transform(transform);
|
||||
Ok(Fragment::frame(frame))
|
||||
|
@ -19,7 +19,7 @@ use self::layout::LayoutRoot;
|
||||
/// Construct the standard library.
|
||||
pub fn build() -> Library {
|
||||
let math = math::module();
|
||||
let calc = compute::calc();
|
||||
let calc = compute::calc::module();
|
||||
let global = global(math.clone(), calc);
|
||||
Library { global, math, styles: styles(), items: items() }
|
||||
}
|
||||
@ -166,37 +166,37 @@ fn items() -> LangItems {
|
||||
layout: |world, content, styles| content.layout_root(world, styles),
|
||||
em: |styles| styles.get(text::TextNode::SIZE),
|
||||
dir: |styles| styles.get(text::TextNode::DIR),
|
||||
space: || text::SpaceNode.pack(),
|
||||
linebreak: || text::LinebreakNode { justify: false }.pack(),
|
||||
text: |text| text::TextNode(text).pack(),
|
||||
space: || text::SpaceNode::new().pack(),
|
||||
linebreak: || text::LinebreakNode::new().pack(),
|
||||
text: |text| text::TextNode::new(text).pack(),
|
||||
text_id: NodeId::of::<text::TextNode>(),
|
||||
text_str: |content| Some(&content.to::<text::TextNode>()?.0),
|
||||
smart_quote: |double| text::SmartQuoteNode { double }.pack(),
|
||||
parbreak: || layout::ParbreakNode.pack(),
|
||||
strong: |body| text::StrongNode(body).pack(),
|
||||
emph: |body| text::EmphNode(body).pack(),
|
||||
text_str: |content| Some(content.to::<text::TextNode>()?.text()),
|
||||
smart_quote: |double| text::SmartQuoteNode::new().with_double(double).pack(),
|
||||
parbreak: || layout::ParbreakNode::new().pack(),
|
||||
strong: |body| text::StrongNode::new(body).pack(),
|
||||
emph: |body| text::EmphNode::new(body).pack(),
|
||||
raw: |text, lang, block| {
|
||||
let content = text::RawNode { text, block }.pack();
|
||||
let content = text::RawNode::new(text).with_block(block).pack();
|
||||
match lang {
|
||||
Some(_) => content.styled(text::RawNode::LANG, lang),
|
||||
None => content,
|
||||
}
|
||||
},
|
||||
link: |url| meta::LinkNode::from_url(url).pack(),
|
||||
ref_: |target| meta::RefNode(target).pack(),
|
||||
heading: |level, body| meta::HeadingNode { level, title: body }.pack(),
|
||||
list_item: |body| layout::ListItem::List(body).pack(),
|
||||
enum_item: |number, body| layout::ListItem::Enum(number, body).pack(),
|
||||
term_item: |term, description| {
|
||||
layout::ListItem::Term(layout::TermItem { term, description }).pack()
|
||||
ref_: |target| meta::RefNode::new(target).pack(),
|
||||
heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(),
|
||||
list_item: |body| layout::ListItem::new(body).pack(),
|
||||
enum_item: |number, body| layout::EnumItem::new(body).with_number(number).pack(),
|
||||
term_item: |term, description| layout::TermItem::new(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_align_point: || math::AlignPointNode.pack(),
|
||||
math_delimited: |open, body, close| {
|
||||
math::LrNode { body: open + body + close, size: None }.pack()
|
||||
math_accent: |base, accent| {
|
||||
math::AccentNode::new(base, math::Accent::new(accent)).pack()
|
||||
},
|
||||
math_attach: |base, bottom, top| math::AttachNode { base, bottom, top }.pack(),
|
||||
math_accent: |base, accent| math::AccentNode { base, accent }.pack(),
|
||||
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
|
||||
math_frac: |num, denom| math::FracNode::new(num, denom).pack(),
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ use super::*;
|
||||
/// How much the accent can be shorter than the base.
|
||||
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||
|
||||
/// # Accent
|
||||
/// Attach an accent to a base.
|
||||
///
|
||||
/// ## Example
|
||||
@ -15,72 +14,48 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||
/// $tilde(a) = accent(a, \u{0303})$
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - base: `Content` (positional, required)
|
||||
/// The base to which the accent is applied.
|
||||
/// 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>`</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)]
|
||||
/// Display: Accent
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
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,
|
||||
/// The accent.
|
||||
pub accent: char,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl AccentNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let base = args.expect("base")?;
|
||||
let accent = args.expect::<Accent>("accent")?.0;
|
||||
Ok(Self { base, accent }.pack())
|
||||
}
|
||||
}
|
||||
|
||||
struct Accent(char);
|
||||
|
||||
castable! {
|
||||
Accent,
|
||||
v: char => Self(v),
|
||||
v: Content => match v.to::<TextNode>() {
|
||||
Some(text) => Self(Value::Str(text.0.clone().into()).cast()?),
|
||||
None => Err("expected text")?,
|
||||
},
|
||||
/// The accent to apply to the base.
|
||||
///
|
||||
/// Supported accents include:
|
||||
///
|
||||
/// | Accent | Name | Codepoint |
|
||||
/// | ------------ | --------------- | --------- |
|
||||
/// | Grave | `grave` | <code>`</code> |
|
||||
/// | Acute | `acute` | `´` |
|
||||
/// | Circumflex | `hat` | `^` |
|
||||
/// | Tilde | `tilde` | `~` |
|
||||
/// | Macron | `macron` | `¯` |
|
||||
/// | Breve | `breve` | `˘` |
|
||||
/// | Dot | `dot` | `.` |
|
||||
/// | Diaeresis | `diaer` | `¨` |
|
||||
/// | Circle | `circle` | `∘` |
|
||||
/// | Double acute | `acute.double` | `˝` |
|
||||
/// | Caron | `caron` | `ˇ` |
|
||||
/// | Right arrow | `arrow`, `->` | `→` |
|
||||
/// | Left arrow | `arrow.l`, `<-` | `←` |
|
||||
#[positional]
|
||||
#[required]
|
||||
pub accent: Accent,
|
||||
}
|
||||
|
||||
impl LayoutMath for AccentNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_cramped(true));
|
||||
let base = ctx.layout_fragment(&self.base)?;
|
||||
let base = ctx.layout_fragment(&self.base())?;
|
||||
ctx.unstyle();
|
||||
|
||||
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
|
||||
// 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 short_fall = ACCENT_SHORT_FALL.scaled(ctx);
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
@ -1,21 +1,11 @@
|
||||
use super::*;
|
||||
|
||||
/// # Alignment Point
|
||||
/// A math alignment point: `&`, `&&`.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - index: `usize` (positional, required)
|
||||
/// The alignment point's index.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct AlignPointNode;
|
||||
|
||||
#[node]
|
||||
impl AlignPointNode {}
|
||||
/// Display: Alignment Point
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct AlignPointNode {}
|
||||
|
||||
impl LayoutMath for AlignPointNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
/// # Attachment
|
||||
/// A base with optional attachments.
|
||||
///
|
||||
/// ## Syntax
|
||||
@ -12,58 +11,44 @@ use super::*;
|
||||
/// $ sum_(i=0)^n a_i = 2^(1+i) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - base: `Content` (positional, required)
|
||||
/// The base to which things are attached.
|
||||
///
|
||||
/// - top: `Content` (named)
|
||||
/// The top attachment.
|
||||
///
|
||||
/// - bottom: `Content` (named)
|
||||
/// The bottom attachment.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Attachment
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct AttachNode {
|
||||
/// The base.
|
||||
/// The base to which things are attached.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub base: Content,
|
||||
/// The top attachment.
|
||||
pub top: Option<Content>,
|
||||
/// The bottom attachment.
|
||||
pub bottom: Option<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl AttachNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let base = args.expect("base")?;
|
||||
let top = args.named("top")?;
|
||||
let bottom = args.named("bottom")?;
|
||||
Ok(Self { base, top, bottom }.pack())
|
||||
}
|
||||
/// The top attachment.
|
||||
#[named]
|
||||
#[default]
|
||||
pub top: Option<Content>,
|
||||
|
||||
/// The bottom attachment.
|
||||
#[named]
|
||||
#[default]
|
||||
pub bottom: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMath for AttachNode {
|
||||
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());
|
||||
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.style(ctx.style.for_superscript());
|
||||
let bottom = self
|
||||
.bottom
|
||||
.as_ref()
|
||||
.map(|node| ctx.layout_fragment(node))
|
||||
.transpose()?;
|
||||
let bottom = self.bottom().map(|node| ctx.layout_fragment(&node)).transpose()?;
|
||||
ctx.unstyle();
|
||||
|
||||
let render_limits = self.base.is::<LimitsNode>()
|
||||
|| (!self.base.is::<ScriptsNode>()
|
||||
let display_limits = display_limits
|
||||
|| (!display_scripts
|
||||
&& ctx.style.size == MathSize::Display
|
||||
&& base.class() == Some(MathClass::Large)
|
||||
&& match &base {
|
||||
@ -72,7 +57,7 @@ impl LayoutMath for AttachNode {
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if render_limits {
|
||||
if display_limits {
|
||||
limits(ctx, base, top, bottom)
|
||||
} else {
|
||||
scripts(ctx, base, top, bottom)
|
||||
@ -80,7 +65,6 @@ impl LayoutMath for AttachNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Scripts
|
||||
/// Force a base to display attachments as scripts.
|
||||
///
|
||||
/// ## Example
|
||||
@ -88,31 +72,22 @@ impl LayoutMath for AttachNode {
|
||||
/// $ scripts(sum)_1^2 != sum_1^2 $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - base: `Content` (positional, required)
|
||||
/// The base to attach the scripts to.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ScriptsNode(Content);
|
||||
|
||||
#[node]
|
||||
impl ScriptsNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("base")?).pack())
|
||||
}
|
||||
/// Display: Scripts
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct ScriptsNode {
|
||||
/// The base to attach the scripts to.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub base: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for ScriptsNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -120,27 +95,19 @@ impl LayoutMath for ScriptsNode {
|
||||
/// $ limits(A)_1^2 != A_1^2 $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - base: `Content` (positional, required)
|
||||
/// The base to attach the limits to.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct LimitsNode(Content);
|
||||
|
||||
#[node]
|
||||
impl LimitsNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("base")?).pack())
|
||||
}
|
||||
/// Display: Limits
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct LimitsNode {
|
||||
/// The base to attach the limits to.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub base: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for LimitsNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
self.0.layout_math(ctx)
|
||||
self.base().layout_math(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ use super::*;
|
||||
/// How much less high scaled delimiters can be than what they wrap.
|
||||
pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||
|
||||
/// # Left/Right
|
||||
/// Scales delimiters.
|
||||
///
|
||||
/// 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%}`.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Left/Right
|
||||
/// Category: math
|
||||
#[node(Construct, LayoutMath)]
|
||||
pub struct LrNode {
|
||||
/// The delimited content, including the delimiters.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
/// The size of the brackets.
|
||||
pub size: Option<Rel<Length>>,
|
||||
#[named]
|
||||
#[default]
|
||||
pub size: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl LrNode {
|
||||
impl Construct for LrNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let mut body = Content::empty();
|
||||
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
|
||||
@ -46,21 +47,21 @@ impl LrNode {
|
||||
}
|
||||
body += arg;
|
||||
}
|
||||
let size = args.named("size")?;
|
||||
Ok(Self { body, size }.pack())
|
||||
let size = args.named::<Smart<Rel<Length>>>("size")?.unwrap_or_default();
|
||||
Ok(Self::new(body).with_size(size).pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMath for LrNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
let mut body = &self.body;
|
||||
if let Some(node) = self.body.to::<LrNode>() {
|
||||
if node.size.is_none() {
|
||||
body = &node.body;
|
||||
let mut body = self.body();
|
||||
if let Some(node) = body.to::<LrNode>() {
|
||||
if node.size().is_auto() {
|
||||
body = node.body();
|
||||
}
|
||||
}
|
||||
|
||||
let mut fragments = ctx.layout_fragments(body)?;
|
||||
let mut fragments = ctx.layout_fragments(&body)?;
|
||||
let axis = scaled!(ctx, axis_height);
|
||||
let max_extent = fragments
|
||||
.iter()
|
||||
@ -69,7 +70,7 @@ impl LayoutMath for LrNode {
|
||||
.unwrap_or_default();
|
||||
|
||||
let height = self
|
||||
.size
|
||||
.size()
|
||||
.unwrap_or(Rel::one())
|
||||
.resolve(ctx.styles())
|
||||
.relative_to(2.0 * max_extent);
|
||||
@ -116,7 +117,6 @@ fn scale(
|
||||
}
|
||||
}
|
||||
|
||||
/// # Floor
|
||||
/// Floor an expression.
|
||||
///
|
||||
/// ## Example
|
||||
@ -128,14 +128,13 @@ fn scale(
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The expression to floor.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
/// Display: Floor
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn floor(args: &mut Args) -> SourceResult<Value> {
|
||||
delimited(args, '⌊', '⌋')
|
||||
}
|
||||
|
||||
/// # Ceil
|
||||
/// Ceil an expression.
|
||||
///
|
||||
/// ## Example
|
||||
@ -147,14 +146,13 @@ pub fn floor(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The expression to ceil.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
/// Display: Ceil
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn ceil(args: &mut Args) -> SourceResult<Value> {
|
||||
delimited(args, '⌈', '⌉')
|
||||
}
|
||||
|
||||
/// # Abs
|
||||
/// Take the absolute value of an expression.
|
||||
///
|
||||
/// ## Example
|
||||
@ -166,14 +164,13 @@ pub fn ceil(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The expression to take the absolute value of.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
/// Display: Abs
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn abs(args: &mut Args) -> SourceResult<Value> {
|
||||
delimited(args, '|', '|')
|
||||
}
|
||||
|
||||
/// # Norm
|
||||
/// Take the norm of an expression.
|
||||
///
|
||||
/// ## Example
|
||||
@ -185,8 +182,8 @@ pub fn abs(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The expression to take the norm of.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
/// Display: Norm
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn norm(args: &mut Args) -> SourceResult<Value> {
|
||||
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> {
|
||||
Ok(Value::Content(
|
||||
LrNode {
|
||||
body: Content::sequence(vec![
|
||||
TextNode::packed(left),
|
||||
args.expect::<Content>("body")?,
|
||||
TextNode::packed(right),
|
||||
]),
|
||||
size: None,
|
||||
}
|
||||
LrNode::new(Content::sequence(vec![
|
||||
TextNode::packed(left),
|
||||
args.expect::<Content>("body")?,
|
||||
TextNode::packed(right),
|
||||
]))
|
||||
.pack(),
|
||||
))
|
||||
}
|
||||
|
@ -17,41 +17,27 @@ const FRAC_AROUND: Em = Em::new(0.1);
|
||||
/// expression using round grouping parenthesis. Such parentheses are removed
|
||||
/// from the output, but you can nest multiple to force them.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - num: `Content` (positional, required)
|
||||
/// The fraction's numerator.
|
||||
///
|
||||
/// - denom: `Content` (positional, required)
|
||||
/// The fraction's denominator.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Fraction
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct FracNode {
|
||||
/// The numerator.
|
||||
/// The fraction's numerator.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub num: Content,
|
||||
/// The denominator.
|
||||
pub denom: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl FracNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let num = args.expect("numerator")?;
|
||||
let denom = args.expect("denominator")?;
|
||||
Ok(Self { num, denom }.pack())
|
||||
}
|
||||
/// The fraction's denominator.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub denom: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for FracNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -59,37 +45,24 @@ impl LayoutMath for FracNode {
|
||||
/// $ binom(n, k) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - upper: `Content` (positional, required)
|
||||
/// The binomial's upper index.
|
||||
///
|
||||
/// - lower: `Content` (positional, required)
|
||||
/// The binomial's lower index.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Binomial
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct BinomNode {
|
||||
/// The upper index.
|
||||
/// The binomial's upper index.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub upper: Content,
|
||||
/// The lower index.
|
||||
pub lower: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl BinomNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let upper = args.expect("upper index")?;
|
||||
let lower = args.expect("lower index")?;
|
||||
Ok(Self { upper, lower }.pack())
|
||||
}
|
||||
/// The binomial's lower index.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub lower: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for BinomNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
layout(ctx, &self.upper, &self.lower, true)
|
||||
layout(ctx, &self.upper(), &self.lower(), true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ const ROW_GAP: Em = Em::new(0.5);
|
||||
const COL_GAP: Em = Em::new(0.5);
|
||||
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||
|
||||
/// # Vector
|
||||
/// A column vector.
|
||||
///
|
||||
/// 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 $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - elements: `Content` (positional, variadic)
|
||||
/// The elements of the vector.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct VecNode(Vec<Content>);
|
||||
/// Display: Vector
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct VecNode {
|
||||
/// The elements of the vector.
|
||||
#[variadic]
|
||||
pub elements: Vec<Content>,
|
||||
|
||||
#[node]
|
||||
impl VecNode {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
/// ```example
|
||||
/// #set math.vec(delim: "[")
|
||||
/// $ vec(1, 2) $
|
||||
/// ```
|
||||
pub const DELIM: Delimiter = Delimiter::Paren;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.all()?).pack())
|
||||
}
|
||||
#[settable]
|
||||
#[default(Delimiter::Paren)]
|
||||
pub delim: Delimiter,
|
||||
}
|
||||
|
||||
impl LayoutMath for VecNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Matrix
|
||||
/// A matrix.
|
||||
///
|
||||
/// The elements of a row should be separated by commas, while the rows
|
||||
@ -70,33 +61,32 @@ impl LayoutMath for VecNode {
|
||||
/// ) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - rows: `Array` (positional, variadic)
|
||||
/// An array of arrays with the rows of the matrix.
|
||||
///
|
||||
/// ```example
|
||||
/// #let data = ((1, 2, 3), (4, 5, 6))
|
||||
/// #let matrix = math.mat(..data)
|
||||
/// $ v := matrix $
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct MatNode(Vec<Vec<Content>>);
|
||||
/// Display: Matrix
|
||||
/// Category: math
|
||||
#[node(Construct, LayoutMath)]
|
||||
pub struct MatNode {
|
||||
/// An array of arrays with the rows of the matrix.
|
||||
///
|
||||
/// ```example
|
||||
/// #let data = ((1, 2, 3), (4, 5, 6))
|
||||
/// #let matrix = math.mat(..data)
|
||||
/// $ v := matrix $
|
||||
/// ```
|
||||
#[variadic]
|
||||
pub rows: Vec<Vec<Content>>,
|
||||
|
||||
#[node]
|
||||
impl MatNode {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
/// ```example
|
||||
/// #set math.mat(delim: "[")
|
||||
/// $ 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> {
|
||||
let mut rows = vec![];
|
||||
let mut width = 0;
|
||||
@ -119,19 +109,18 @@ impl MatNode {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(rows).pack())
|
||||
Ok(Self::new(rows).pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMath for MatNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Cases
|
||||
/// A case distinction.
|
||||
///
|
||||
/// Content across different branches can be aligned with the `&` symbol.
|
||||
@ -146,36 +135,29 @@ impl LayoutMath for MatNode {
|
||||
/// ) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - branches: `Content` (positional, variadic)
|
||||
/// The branches of the case distinction.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct CasesNode(Vec<Content>);
|
||||
/// Display: Cases
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct CasesNode {
|
||||
/// The branches of the case distinction.
|
||||
#[variadic]
|
||||
pub branches: Vec<Content>,
|
||||
|
||||
#[node]
|
||||
impl CasesNode {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
/// ```example
|
||||
/// #set math.cases(delim: "[")
|
||||
/// $ x = cases(1, 2) $
|
||||
/// ```
|
||||
pub const DELIM: Delimiter = Delimiter::Brace;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.all()?).pack())
|
||||
}
|
||||
#[settable]
|
||||
#[default(Delimiter::Brace)]
|
||||
pub delim: Delimiter,
|
||||
}
|
||||
|
||||
impl LayoutMath for CasesNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -214,7 +196,7 @@ impl Delimiter {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Delimiter,
|
||||
/// Delimit with parentheses.
|
||||
"(" => Self::Paren,
|
||||
@ -228,6 +210,16 @@ castable! {
|
||||
"||" => 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.
|
||||
fn layout_vec_body(
|
||||
ctx: &mut MathContext,
|
||||
|
@ -107,7 +107,6 @@ pub fn module() -> Module {
|
||||
Module::new("math").with_scope(math)
|
||||
}
|
||||
|
||||
/// # Formula
|
||||
/// A mathematical formula.
|
||||
///
|
||||
/// 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
|
||||
/// [main math page]($category/math).
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The contents of the formula.
|
||||
///
|
||||
/// - block: `bool` (named)
|
||||
/// Whether the formula is displayed as a separate block.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(Show, Finalize, Layout, LayoutMath)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
/// Display: Formula
|
||||
/// Category: math
|
||||
#[node(Show, Finalize, Layout, LayoutMath)]
|
||||
pub struct FormulaNode {
|
||||
/// Whether the formula is displayed as a separate block.
|
||||
pub block: bool,
|
||||
/// The content of the formula.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl FormulaNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let body = args.expect("body")?;
|
||||
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,
|
||||
}
|
||||
}
|
||||
/// Whether the formula is displayed as a separate block.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
pub block: bool,
|
||||
}
|
||||
|
||||
impl Show for FormulaNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
|
||||
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())))
|
||||
}
|
||||
Ok(realized)
|
||||
@ -196,27 +174,29 @@ impl Layout for FormulaNode {
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let block = self.block();
|
||||
|
||||
// Find a math font.
|
||||
let variant = variant(styles);
|
||||
let world = vt.world();
|
||||
let Some(font) = families(styles)
|
||||
.find_map(|family| {
|
||||
let id = world.book().select(family, variant)?;
|
||||
let id = world.book().select(family.as_str(), variant)?;
|
||||
let font = world.font(id)?;
|
||||
let _ = font.ttf().tables().math?.constants?;
|
||||
Some(font)
|
||||
})
|
||||
else {
|
||||
if let Some(span) = self.body.span() {
|
||||
if let Some(span) = self.span() {
|
||||
bail!(span, "current font does not support math");
|
||||
}
|
||||
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)?;
|
||||
|
||||
if !self.block {
|
||||
if !block {
|
||||
let slack = styles.get(ParNode::LEADING) * 0.7;
|
||||
let top_edge = styles.get(TextNode::TOP_EDGE).resolve(styles, font.metrics());
|
||||
let bottom_edge =
|
||||
@ -232,38 +212,38 @@ impl Layout for FormulaNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[capability]
|
||||
pub trait LayoutMath {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
|
||||
}
|
||||
|
||||
impl LayoutMath for FormulaNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
self.body.layout_math(ctx)
|
||||
self.body().layout_math(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMath for Content {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
if let Some(node) = self.to::<SequenceNode>() {
|
||||
for child in &node.0 {
|
||||
for child in node.children() {
|
||||
child.layout_math(ctx)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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)?;
|
||||
ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
|
||||
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;
|
||||
ctx.map.apply(prev_map.clone());
|
||||
ctx.size = ctx.styles().get(TextNode::SIZE);
|
||||
styled.sub.layout_math(ctx)?;
|
||||
styled.sub().layout_math(ctx)?;
|
||||
ctx.size = prev_size;
|
||||
ctx.map = prev_map;
|
||||
return Ok(());
|
||||
@ -280,7 +260,7 @@ impl LayoutMath for Content {
|
||||
}
|
||||
|
||||
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() {
|
||||
ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles())));
|
||||
}
|
||||
@ -289,7 +269,7 @@ impl LayoutMath for Content {
|
||||
}
|
||||
|
||||
if let Some(node) = self.to::<TextNode>() {
|
||||
ctx.layout_text(&node.0)?;
|
||||
ctx.layout_text(&node.text())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ use typst::eval::Scope;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// # Text Operator
|
||||
/// A text operator in a math formula.
|
||||
///
|
||||
/// ## Example
|
||||
@ -19,45 +18,30 @@ use super::*;
|
||||
/// `max`, `min`, `Pr`, `sec`, `sin`, `sinh`, `sup`, `tan`, `tg`, `tanh`,
|
||||
/// `liminf`, and `limsup`.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - text: `EcoString` (positional, required)
|
||||
/// The operator's text.
|
||||
///
|
||||
/// - limits: `bool` (named)
|
||||
/// Whether the operator should force attachments to display as limits.
|
||||
///
|
||||
/// Defaults to `{false}`.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Text Operator
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct OpNode {
|
||||
/// The operator's text.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub text: EcoString,
|
||||
/// Whether the operator should force attachments to display as limits.
|
||||
pub limits: bool,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl OpNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self {
|
||||
text: args.expect("text")?,
|
||||
limits: args.named("limits")?.unwrap_or(false),
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
/// Whether the operator should force attachments to display as limits.
|
||||
///
|
||||
/// Defaults to `{false}`.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
pub limits: bool,
|
||||
}
|
||||
|
||||
impl LayoutMath for OpNode {
|
||||
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(
|
||||
FrameFragment::new(ctx, frame)
|
||||
.with_class(MathClass::Large)
|
||||
.with_limits(self.limits),
|
||||
.with_limits(self.limits()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@ -68,13 +52,15 @@ macro_rules! ops {
|
||||
pub(super) fn define(math: &mut Scope) {
|
||||
$(math.define(
|
||||
stringify!($name),
|
||||
OpNode {
|
||||
text: ops!(@name $name $(: $value)?).into(),
|
||||
limits: ops!(@limit $($tts)*),
|
||||
}.pack()
|
||||
OpNode::new(ops!(@name $name $(: $value)?).into())
|
||||
.with_limits(ops!(@limit $($tts)*))
|
||||
.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'));
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
/// # Square Root
|
||||
/// A square root.
|
||||
///
|
||||
/// ## Example
|
||||
@ -8,31 +7,22 @@ use super::*;
|
||||
/// $ sqrt(x^2) = x = sqrt(x)^2 $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - radicand: `Content` (positional, required)
|
||||
/// The expression to take the square root of.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Square Root
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct SqrtNode {
|
||||
/// The expression to take the square root of.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub radicand: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for SqrtNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
layout(ctx, None, &self.0)
|
||||
layout(ctx, None, &self.radicand())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Root
|
||||
/// A general root.
|
||||
///
|
||||
/// ## Example
|
||||
@ -40,37 +30,24 @@ impl LayoutMath for SqrtNode {
|
||||
/// $ root(3, x) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - index: `Content` (positional, required)
|
||||
/// Which root of the radicand to take.
|
||||
///
|
||||
/// - radicand: `Content` (positional, required)
|
||||
/// The expression to take the root of.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Root
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct RootNode {
|
||||
/// Which root of the radicand to take.
|
||||
#[positional]
|
||||
#[required]
|
||||
index: Content,
|
||||
radicand: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl RootNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self {
|
||||
index: args.expect("index")?,
|
||||
radicand: args.expect("radicand")?,
|
||||
}
|
||||
.pack())
|
||||
}
|
||||
/// The expression to take the root of.
|
||||
#[positional]
|
||||
#[required]
|
||||
radicand: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for RootNode {
|
||||
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.
|
||||
fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> {
|
||||
let node = index?.to::<TextNode>()?;
|
||||
let c = match node.0.as_str() {
|
||||
let c = match node.text().as_str() {
|
||||
"3" => '∛',
|
||||
"4" => '∜',
|
||||
_ => return None,
|
||||
|
@ -103,7 +103,7 @@ impl MathRow {
|
||||
|
||||
pub fn to_frame(self, ctx: &MathContext) -> Frame {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -200,10 +200,7 @@ impl MathRow {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MathRow
|
||||
where
|
||||
T: Into<MathFragment>,
|
||||
{
|
||||
impl<T: Into<MathFragment>> From<T> for MathRow {
|
||||
fn from(fragment: T) -> Self {
|
||||
Self(vec![fragment.into()])
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ pub(super) const QUAD: Em = Em::new(1.0);
|
||||
|
||||
/// Hook up all spacings.
|
||||
pub(super) fn define(math: &mut Scope) {
|
||||
math.define("thin", HNode::strong(THIN).pack());
|
||||
math.define("med", HNode::strong(MEDIUM).pack());
|
||||
math.define("thick", HNode::strong(THICK).pack());
|
||||
math.define("quad", HNode::strong(QUAD).pack());
|
||||
math.define("thin", HNode::new(THIN.into()).pack());
|
||||
math.define("med", HNode::new(MEDIUM.into()).pack());
|
||||
math.define("thick", HNode::new(THICK.into()).pack());
|
||||
math.define("quad", HNode::new(QUAD.into()).pack());
|
||||
}
|
||||
|
||||
/// Create the spacing between two fragments in a given style.
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
/// # Bold
|
||||
/// Bold font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -8,34 +7,25 @@ use super::*;
|
||||
/// $ bold(A) := B^+ $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Bold
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct BoldNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for BoldNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_bold(true));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Upright
|
||||
/// Upright (non-italic) font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -43,98 +33,71 @@ impl LayoutMath for BoldNode {
|
||||
/// $ upright(A) != A $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Upright
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct UprightNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for UprightNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_italic(false));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Italic
|
||||
/// Italic font style in math.
|
||||
///
|
||||
/// For roman letters and greek lowercase letters, this is already the default.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Italic
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct ItalicNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for ItalicNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_italic(true));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Serif
|
||||
/// Serif (roman) font style in math.
|
||||
///
|
||||
/// This is already the default.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Serif
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct SerifNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for SerifNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Serif));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Sans-serif
|
||||
/// Sans-serif font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -142,34 +105,25 @@ impl LayoutMath for SerifNode {
|
||||
/// $ sans(A B C) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Sans-serif
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct SansNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for SansNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Sans));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Calligraphic
|
||||
/// Calligraphic font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -177,34 +131,25 @@ impl LayoutMath for SansNode {
|
||||
/// Let $cal(P)$ be the set of ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Calligraphic
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct CalNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for CalNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Cal));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Fraktur
|
||||
/// Fraktur font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -212,34 +157,25 @@ impl LayoutMath for CalNode {
|
||||
/// $ frak(P) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Fraktur
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct FrakNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for FrakNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Frak));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Monospace
|
||||
/// Monospace font style in math.
|
||||
///
|
||||
/// ## Example
|
||||
@ -247,34 +183,25 @@ impl LayoutMath for FrakNode {
|
||||
/// $ mono(x + y = z) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Monospace
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct MonoNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for MonoNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Mono));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Blackboard Bold
|
||||
/// Blackboard bold (double-struck) font style in math.
|
||||
///
|
||||
/// For uppercase latin letters, blackboard bold is additionally available
|
||||
@ -287,28 +214,20 @@ impl LayoutMath for MonoNode {
|
||||
/// $ f: NN -> RR $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The piece of formula to style.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[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())
|
||||
}
|
||||
/// Display: Blackboard Bold
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct BbNode {
|
||||
/// The piece of formula to style.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for BbNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
ctx.style(ctx.style.with_variant(MathVariant::Bb));
|
||||
self.0.layout_math(ctx)?;
|
||||
self.body().layout_math(ctx)?;
|
||||
ctx.unstyle();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ const LINE_GAP: Em = Em::new(0.15);
|
||||
const BRACE_GAP: Em = Em::new(0.25);
|
||||
const BRACKET_GAP: Em = Em::new(0.25);
|
||||
|
||||
/// # Underline
|
||||
/// A horizontal line under content.
|
||||
///
|
||||
/// ## Example
|
||||
@ -12,31 +11,22 @@ const BRACKET_GAP: Em = Em::new(0.25);
|
||||
/// $ underline(1 + 2 + ... + 5) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content above the line.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct UnderlineNode(Content);
|
||||
|
||||
#[node]
|
||||
impl UnderlineNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("body")?).pack())
|
||||
}
|
||||
/// Display: Underline
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct UnderlineNode {
|
||||
/// The content above the line.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for UnderlineNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -44,31 +34,22 @@ impl LayoutMath for UnderlineNode {
|
||||
/// $ overline(1 + 2 + ... + 5) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content below the line.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct OverlineNode(Content);
|
||||
|
||||
#[node]
|
||||
impl OverlineNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("body")?).pack())
|
||||
}
|
||||
/// Display: Overline
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct OverlineNode {
|
||||
/// The content below the line.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMath for OverlineNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -76,41 +57,27 @@ impl LayoutMath for OverlineNode {
|
||||
/// $ underbrace(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content above the brace.
|
||||
///
|
||||
/// - annotation: `Content` (positional)
|
||||
/// The optional content below the brace.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Underbrace
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct UnderbraceNode {
|
||||
/// The content above the brace.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
/// The optional content below the brace.
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl UnderbraceNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let body = args.expect("body")?;
|
||||
let annotation = args.eat()?;
|
||||
Ok(Self { body, annotation }.pack())
|
||||
}
|
||||
/// The optional content below the brace.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMath for UnderbraceNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -118,41 +85,27 @@ impl LayoutMath for UnderbraceNode {
|
||||
/// $ overbrace(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content below the brace.
|
||||
///
|
||||
/// - annotation: `Content` (positional)
|
||||
/// The optional content above the brace.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Overbrace
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct OverbraceNode {
|
||||
/// The content below the brace.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
/// The optional content above the brace.
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl OverbraceNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let body = args.expect("body")?;
|
||||
let annotation = args.eat()?;
|
||||
Ok(Self { body, annotation }.pack())
|
||||
}
|
||||
/// The optional content above the brace.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMath for OverbraceNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -160,41 +113,27 @@ impl LayoutMath for OverbraceNode {
|
||||
/// $ underbracket(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content above the bracket.
|
||||
///
|
||||
/// - annotation: `Content` (positional)
|
||||
/// The optional content below the bracket.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Underbracket
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct UnderbracketNode {
|
||||
/// The content above the bracket.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
/// The optional content below the bracket.
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl UnderbracketNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let body = args.expect("body")?;
|
||||
let annotation = args.eat()?;
|
||||
Ok(Self { body, annotation }.pack())
|
||||
}
|
||||
/// The optional content below the bracket.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMath for UnderbracketNode {
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@ -202,37 +141,24 @@ impl LayoutMath for UnderbracketNode {
|
||||
/// $ overbracket(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content below the bracket.
|
||||
///
|
||||
/// - annotation: `Content` (positional)
|
||||
/// The optional content above the bracket.
|
||||
///
|
||||
/// ## Category
|
||||
/// math
|
||||
#[func]
|
||||
#[capable(LayoutMath)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Overbracket
|
||||
/// Category: math
|
||||
#[node(LayoutMath)]
|
||||
pub struct OverbracketNode {
|
||||
/// The content below the bracket.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
/// The optional content above the bracket.
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl OverbracketNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let body = args.expect("body")?;
|
||||
let annotation = args.eat()?;
|
||||
Ok(Self { body, annotation }.pack())
|
||||
}
|
||||
/// The optional content above the bracket.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub annotation: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMath for OverbracketNode {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
use typst::model::StyledNode;
|
||||
|
||||
use crate::layout::{LayoutRoot, PageNode};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Document
|
||||
/// The root element of a document and its metadata.
|
||||
///
|
||||
/// 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.
|
||||
/// Instead, it is embedded in the compiled PDF file.
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
#[func]
|
||||
#[capable(LayoutRoot)]
|
||||
#[derive(Hash)]
|
||||
pub struct DocumentNode(pub StyleVec<PageNode>);
|
||||
/// Display: Document
|
||||
/// Category: meta
|
||||
#[node(LayoutRoot)]
|
||||
pub struct DocumentNode {
|
||||
/// The page runs.
|
||||
#[variadic]
|
||||
pub children: Vec<Content>,
|
||||
|
||||
#[node]
|
||||
impl DocumentNode {
|
||||
/// The document's title. This is often rendered as the title of the
|
||||
/// PDF viewer window.
|
||||
#[property(referenced)]
|
||||
pub const TITLE: Option<EcoString> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub title: Option<EcoString>,
|
||||
|
||||
/// The document's authors.
|
||||
#[property(referenced)]
|
||||
pub const AUTHOR: Author = Author(vec![]);
|
||||
#[settable]
|
||||
#[default]
|
||||
pub author: Author,
|
||||
}
|
||||
|
||||
impl LayoutRoot for DocumentNode {
|
||||
/// Layout the document into a sequence of frames, one per page.
|
||||
fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
|
||||
let mut pages = vec![];
|
||||
for (page, map) in self.0.iter() {
|
||||
let number = 1 + pages.len();
|
||||
let fragment = page.layout(vt, number, styles.chain(map))?;
|
||||
pages.extend(fragment);
|
||||
|
||||
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 fragment = page.layout(vt, number, styles)?;
|
||||
pages.extend(fragment);
|
||||
} else if let Some(span) = child.span() {
|
||||
bail!(span, "unexpected document child");
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
#[derive(Debug, Default, Clone, Hash)]
|
||||
pub struct Author(Vec<EcoString>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Author,
|
||||
v: EcoString => Self(vec![v]),
|
||||
v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Author => v.0.into()
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ use crate::layout::{BlockNode, HNode, VNode};
|
||||
use crate::prelude::*;
|
||||
use crate::text::{TextNode, TextSize};
|
||||
|
||||
/// # Heading
|
||||
/// A section 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
|
||||
/// signs determines the heading's logical nesting depth.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - title: `Content` (positional, required)
|
||||
/// The heading's title.
|
||||
///
|
||||
/// - level: `NonZeroUsize` (named)
|
||||
/// The logical nesting depth of the heading, starting from one.
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
#[func]
|
||||
#[capable(Prepare, Show, Finalize)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Heading
|
||||
/// Category: meta
|
||||
#[node(Prepare, Show, Finalize)]
|
||||
pub struct HeadingNode {
|
||||
/// The logical nesting depth of the section, starting from one. In the
|
||||
/// default style, this controls the text size of the heading.
|
||||
pub level: NonZeroUsize,
|
||||
/// The heading's contents.
|
||||
/// The heading's title.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub title: Content,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl HeadingNode {
|
||||
/// The logical nesting depth of the heading, starting from one.
|
||||
#[named]
|
||||
#[default(NonZeroUsize::new(1).unwrap())]
|
||||
pub level: NonZeroUsize,
|
||||
|
||||
/// How to number the heading. Accepts a
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
///
|
||||
@ -71,8 +62,9 @@ impl HeadingNode {
|
||||
/// == A subsection
|
||||
/// === A sub-subsection
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const NUMBERING: Option<Numbering> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub numbering: Option<Numbering>,
|
||||
|
||||
/// Whether the heading should appear in the outline.
|
||||
///
|
||||
@ -86,23 +78,9 @@ impl HeadingNode {
|
||||
/// This heading does not appear
|
||||
/// in the outline.
|
||||
/// ```
|
||||
pub const OUTLINED: bool = true;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub outlined: bool,
|
||||
}
|
||||
|
||||
impl Prepare for HeadingNode {
|
||||
@ -121,7 +99,7 @@ impl Prepare for HeadingNode {
|
||||
}
|
||||
|
||||
let numbers = node.field("numbers").unwrap();
|
||||
if numbers != Value::None {
|
||||
if *numbers != Value::None {
|
||||
let heading = node.to::<Self>().unwrap();
|
||||
counter.advance(heading);
|
||||
}
|
||||
@ -136,38 +114,34 @@ impl Prepare for HeadingNode {
|
||||
this.push_field("numbers", numbers);
|
||||
|
||||
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 {
|
||||
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();
|
||||
if numbers != Value::None {
|
||||
realized = numbers.display()
|
||||
+ HNode { amount: Em::new(0.3).into(), weak: true }.pack()
|
||||
if *numbers != Value::None {
|
||||
realized = numbers.clone().display()
|
||||
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack()
|
||||
+ realized;
|
||||
}
|
||||
Ok(BlockNode {
|
||||
body: realized,
|
||||
width: Smart::Auto,
|
||||
height: Smart::Auto,
|
||||
}
|
||||
.pack())
|
||||
Ok(BlockNode::new().with_body(realized).pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for HeadingNode {
|
||||
fn finalize(&self, realized: Content) -> Content {
|
||||
let scale = match self.level.get() {
|
||||
let level = self.level().get();
|
||||
let scale = match level {
|
||||
1 => 1.4,
|
||||
2 => 1.2,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
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 mut map = StyleMap::new();
|
||||
@ -191,7 +165,7 @@ impl HeadingCounter {
|
||||
|
||||
/// Advance the counter and return the numbers for the given heading.
|
||||
pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] {
|
||||
let level = heading.level.get();
|
||||
let level = heading.level().get();
|
||||
|
||||
if self.0.len() >= level {
|
||||
self.0[level - 1] = self.0[level - 1].saturating_add(1);
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::text::{Hyphenate, TextNode};
|
||||
|
||||
/// # Link
|
||||
/// Link to a URL or another location in the document.
|
||||
///
|
||||
/// 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
|
||||
/// parameter can be omitted. In this case, the URL will be shown as the link.
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
#[func]
|
||||
#[capable(Show, Finalize)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Link
|
||||
/// Category: meta
|
||||
#[node(Construct, Show, Finalize)]
|
||||
pub struct LinkNode {
|
||||
/// The destination the link points to.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub dest: Destination,
|
||||
|
||||
/// How the link is represented.
|
||||
#[positional]
|
||||
#[default]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LinkNode {
|
||||
/// Create a link node from a URL with its bare text.
|
||||
pub fn from_url(url: EcoString) -> Self {
|
||||
let mut text = url.as_str();
|
||||
for prefix in ["mailto:", "tel:"] {
|
||||
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 }
|
||||
let body = body_from_url(&url);
|
||||
Self::new(Destination::Url(url)).with_body(body)
|
||||
}
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl LinkNode {
|
||||
impl Construct for LinkNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let dest = args.expect::<Destination>("destination")?;
|
||||
Ok(match dest {
|
||||
let body = match &dest {
|
||||
Destination::Url(url) => match args.eat()? {
|
||||
Some(body) => Self { dest: Destination::Url(url), body },
|
||||
None => Self::from_url(url),
|
||||
Some(body) => body,
|
||||
None => body_from_url(url),
|
||||
},
|
||||
Destination::Internal(_) => Self { dest, body: args.expect("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,
|
||||
}
|
||||
Destination::Internal(_) => args.expect("body")?,
|
||||
};
|
||||
Ok(Self::new(dest).with_body(body).pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for LinkNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.body.clone())
|
||||
Ok(self.body())
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for LinkNode {
|
||||
fn finalize(&self, realized: Content) -> Content {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
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() })
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ use std::str::FromStr;
|
||||
use crate::prelude::*;
|
||||
use crate::text::Case;
|
||||
|
||||
/// # Numbering
|
||||
/// Apply a numbering to a sequence of numbers.
|
||||
///
|
||||
/// A numbering defines how a sequence of numbers should be displayed as
|
||||
@ -61,8 +60,8 @@ use crate::text::Case;
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
/// Display: Numbering
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn numbering(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
|
||||
let numbering = args.expect::<Numbering>("pattern or function")?;
|
||||
@ -99,12 +98,19 @@ impl Numbering {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Numbering,
|
||||
v: Str => Self::Pattern(v.parse()?),
|
||||
v: NumberingPattern => Self::Pattern(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.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
for (i, c) in pattern.char_indices() {
|
||||
let kind = match c.to_ascii_lowercase() {
|
||||
'1' => NumberingKind::Arabic,
|
||||
'a' => NumberingKind::Letter,
|
||||
'i' => NumberingKind::Roman,
|
||||
'*' => NumberingKind::Symbol,
|
||||
_ => continue,
|
||||
let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
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.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
enum NumberingKind {
|
||||
@ -206,6 +229,27 @@ enum 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.
|
||||
pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString {
|
||||
let mut n = n.get();
|
||||
|
@ -1,11 +1,8 @@
|
||||
use super::HeadingNode;
|
||||
use crate::layout::{
|
||||
BoxNode, HNode, HideNode, ParbreakNode, RepeatNode, Sizing, Spacing,
|
||||
};
|
||||
use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode};
|
||||
use crate::prelude::*;
|
||||
use crate::text::{LinebreakNode, SpaceNode, TextNode};
|
||||
|
||||
/// # Outline
|
||||
/// A section outline / table of contents.
|
||||
///
|
||||
/// 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)
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
#[func]
|
||||
#[capable(Prepare, Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct OutlineNode;
|
||||
|
||||
#[node]
|
||||
impl OutlineNode {
|
||||
/// Display: Outline
|
||||
/// Category: meta
|
||||
#[node(Prepare, Show)]
|
||||
pub struct OutlineNode {
|
||||
/// The title of the outline.
|
||||
///
|
||||
/// - When set to `{auto}`, an appropriate title for the [text
|
||||
/// language]($func/text.lang) will be used. This is the default.
|
||||
/// - When set to `{none}`, the outline will not have a title.
|
||||
/// - A custom title can be set by passing content.
|
||||
#[property(referenced)]
|
||||
pub const TITLE: Option<Smart<Content>> = Some(Smart::Auto);
|
||||
#[settable]
|
||||
#[default(Some(Smart::Auto))]
|
||||
pub title: Option<Smart<Content>>,
|
||||
|
||||
/// The maximum depth up to which headings are included in the outline. When
|
||||
/// 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
|
||||
/// with the title of their parents. This will only have an effect if a
|
||||
@ -62,7 +57,9 @@ impl OutlineNode {
|
||||
/// == Products
|
||||
/// #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
|
||||
/// set to `none` to disable filling. The default is `{repeat[.]}`.
|
||||
@ -72,12 +69,9 @@ impl OutlineNode {
|
||||
///
|
||||
/// = A New Beginning
|
||||
/// ```
|
||||
#[property(referenced)]
|
||||
pub const FILL: Option<Content> = Some(RepeatNode(TextNode::packed(".")).pack());
|
||||
|
||||
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self.pack())
|
||||
}
|
||||
#[settable]
|
||||
#[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))]
|
||||
pub fill: Option<Content>,
|
||||
}
|
||||
|
||||
impl Prepare for OutlineNode {
|
||||
@ -91,7 +85,7 @@ impl Prepare for OutlineNode {
|
||||
.locate(Selector::node::<HeadingNode>())
|
||||
.into_iter()
|
||||
.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()))
|
||||
.collect();
|
||||
|
||||
@ -107,7 +101,7 @@ impl Show for OutlineNode {
|
||||
_: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Content> {
|
||||
let mut seq = vec![ParbreakNode.pack()];
|
||||
let mut seq = vec![ParbreakNode::new().pack()];
|
||||
if let Some(title) = styles.get(Self::TITLE) {
|
||||
let body = title.clone().unwrap_or_else(|| {
|
||||
TextNode::packed(match styles.get(TextNode::LANG) {
|
||||
@ -117,7 +111,7 @@ impl Show for OutlineNode {
|
||||
});
|
||||
|
||||
seq.push(
|
||||
HeadingNode { title: body, level: NonZeroUsize::new(1).unwrap() }
|
||||
HeadingNode::new(body)
|
||||
.pack()
|
||||
.styled(HeadingNode::NUMBERING, None)
|
||||
.styled(HeadingNode::OUTLINED, false),
|
||||
@ -129,26 +123,26 @@ impl Show for OutlineNode {
|
||||
|
||||
let mut ancestors: Vec<&Content> = vec![];
|
||||
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;
|
||||
}
|
||||
|
||||
let heading = node.to::<HeadingNode>().unwrap();
|
||||
if let Some(depth) = depth {
|
||||
if depth < heading.level {
|
||||
if depth < heading.level() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
while ancestors.last().map_or(false, |last| {
|
||||
last.to::<HeadingNode>().unwrap().level >= heading.level
|
||||
last.to::<HeadingNode>().unwrap().level() >= heading.level()
|
||||
}) {
|
||||
ancestors.pop();
|
||||
}
|
||||
|
||||
// Adjust the link destination a bit to the topleft so that the
|
||||
// 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));
|
||||
|
||||
// Add hidden ancestors numberings to realize the indent.
|
||||
@ -156,21 +150,21 @@ impl Show for OutlineNode {
|
||||
let hidden: Vec<_> = ancestors
|
||||
.iter()
|
||||
.map(|node| node.field("numbers").unwrap())
|
||||
.filter(|numbers| *numbers != Value::None)
|
||||
.map(|numbers| numbers.display() + SpaceNode.pack())
|
||||
.filter(|&numbers| *numbers != Value::None)
|
||||
.map(|numbers| numbers.clone().display() + SpaceNode::new().pack())
|
||||
.collect();
|
||||
|
||||
if !hidden.is_empty() {
|
||||
seq.push(HideNode(Content::sequence(hidden)).pack());
|
||||
seq.push(SpaceNode.pack());
|
||||
seq.push(HideNode::new(Content::sequence(hidden)).pack());
|
||||
seq.push(SpaceNode::new().pack());
|
||||
}
|
||||
}
|
||||
|
||||
// Format the numbering.
|
||||
let mut start = heading.title.clone();
|
||||
let mut start = heading.title();
|
||||
let numbers = node.field("numbers").unwrap();
|
||||
if numbers != Value::None {
|
||||
start = numbers.display() + SpaceNode.pack() + start;
|
||||
if *numbers != Value::None {
|
||||
start = numbers.clone().display() + SpaceNode::new().pack() + start;
|
||||
};
|
||||
|
||||
// Add the numbering and section name.
|
||||
@ -178,30 +172,27 @@ impl Show for OutlineNode {
|
||||
|
||||
// Add filler symbols between the section name and page number.
|
||||
if let Some(filler) = styles.get(Self::FILL) {
|
||||
seq.push(SpaceNode.pack());
|
||||
seq.push(SpaceNode::new().pack());
|
||||
seq.push(
|
||||
BoxNode {
|
||||
body: filler.clone(),
|
||||
width: Sizing::Fr(Fr::one()),
|
||||
height: Smart::Auto,
|
||||
}
|
||||
.pack(),
|
||||
BoxNode::new()
|
||||
.with_body(filler.clone())
|
||||
.with_width(Fr::one().into())
|
||||
.pack(),
|
||||
);
|
||||
seq.push(SpaceNode.pack());
|
||||
seq.push(SpaceNode::new().pack());
|
||||
} else {
|
||||
let amount = Spacing::Fr(Fr::one());
|
||||
seq.push(HNode { amount, weak: false }.pack());
|
||||
seq.push(HNode::new(Fr::one().into()).pack());
|
||||
}
|
||||
|
||||
// Add the page number and linebreak.
|
||||
let end = TextNode::packed(eco_format!("{}", loc.page));
|
||||
seq.push(end.linked(Destination::Internal(loc)));
|
||||
seq.push(LinebreakNode { justify: false }.pack());
|
||||
seq.push(LinebreakNode::new().pack());
|
||||
|
||||
ancestors.push(node);
|
||||
}
|
||||
|
||||
seq.push(ParbreakNode.pack());
|
||||
seq.push(ParbreakNode::new().pack());
|
||||
|
||||
Ok(Content::sequence(seq))
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
/// # Reference
|
||||
/// A reference to a label.
|
||||
///
|
||||
/// *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. `[=
|
||||
/// Introduction <intro>]` can be referenced by typing `[@intro]`).
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - target: `Label` (positional, required)
|
||||
/// The label that should be referenced.
|
||||
///
|
||||
/// ## Category
|
||||
/// meta
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
/// Display: Reference
|
||||
/// Category: meta
|
||||
#[node(Show)]
|
||||
pub struct RefNode {
|
||||
/// The label that should be referenced.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub target: EcoString,
|
||||
}
|
||||
|
||||
impl Show for RefNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(TextNode::packed(eco_format!("@{}", self.0)))
|
||||
Ok(TextNode::packed(eco_format!("@{}", self.target())))
|
||||
}
|
||||
}
|
||||
|
@ -15,16 +15,16 @@ pub use typst::diag::{bail, error, At, SourceResult, StrResult};
|
||||
pub use typst::doc::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::eval::{
|
||||
array, castable, dict, format_str, func, Args, Array, AutoValue, Cast, CastInfo,
|
||||
Dict, Func, NoneValue, Str, Symbol, Value, Vm,
|
||||
array, cast_from_value, cast_to_value, dict, format_str, func, Args, Array, Cast,
|
||||
CastInfo, Dict, Func, Never, Str, Symbol, Value, Vm,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use typst::geom::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::model::{
|
||||
capability, capable, node, Content, Finalize, Fold, Introspector, Label, Node,
|
||||
NodeId, Prepare, Resolve, Selector, Show, StabilityProvider, StyleChain, StyleMap,
|
||||
StyleVec, Unlabellable, Vt,
|
||||
node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Prepare,
|
||||
Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec,
|
||||
Unlabellable, Vt,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use typst::syntax::{Span, Spanned};
|
||||
|
@ -1,9 +1,8 @@
|
||||
//! Node interaction.
|
||||
|
||||
use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder};
|
||||
use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder};
|
||||
|
||||
/// How a node interacts with other nodes.
|
||||
#[capability]
|
||||
pub trait Behave {
|
||||
/// The node's interaction behaviour.
|
||||
fn behaviour(&self) -> Behaviour;
|
||||
@ -23,7 +22,7 @@ pub enum Behaviour {
|
||||
/// after it. Furthermore, per consecutive run of weak nodes, only one
|
||||
/// survives: The one with the lowest weakness level (or the larger one if
|
||||
/// there is a tie).
|
||||
Weak(u8),
|
||||
Weak(usize),
|
||||
/// A node that enables adjacent weak nodes to exist. The default.
|
||||
Supportive,
|
||||
/// A node that destroys adjacent weak nodes.
|
||||
|
@ -24,49 +24,43 @@ pub trait ContentExt {
|
||||
|
||||
/// Transform this content's contents without affecting layout.
|
||||
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 {
|
||||
fn strong(self) -> Self {
|
||||
crate::text::StrongNode(self).pack()
|
||||
crate::text::StrongNode::new(self).pack()
|
||||
}
|
||||
|
||||
fn emph(self) -> Self {
|
||||
crate::text::EmphNode(self).pack()
|
||||
crate::text::EmphNode::new(self).pack()
|
||||
}
|
||||
|
||||
fn underlined(self) -> Self {
|
||||
crate::text::UnderlineNode(self).pack()
|
||||
crate::text::UnderlineNode::new(self).pack()
|
||||
}
|
||||
|
||||
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 {
|
||||
self.styled(crate::layout::AlignNode::ALIGNS, aligns)
|
||||
self.styled(crate::layout::AlignNode::ALIGNMENT, aligns)
|
||||
}
|
||||
|
||||
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 {
|
||||
crate::layout::MoveNode { delta, body: self }.pack()
|
||||
}
|
||||
|
||||
fn filled(self, fill: Paint) -> Self {
|
||||
FillNode { fill, child: self }.pack()
|
||||
}
|
||||
|
||||
fn stroked(self, stroke: Stroke) -> Self {
|
||||
StrokeNode { stroke, child: self }.pack()
|
||||
crate::layout::MoveNode::new(self)
|
||||
.with_dx(delta.x)
|
||||
.with_dy(delta.y)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use super::TextNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Underline
|
||||
/// Underline text.
|
||||
///
|
||||
/// ## Example
|
||||
@ -12,19 +11,15 @@ use crate::prelude::*;
|
||||
/// This is #underline[important].
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to underline.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct UnderlineNode(pub Content);
|
||||
/// Display: Underline
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct UnderlineNode {
|
||||
/// The content to underline.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl UnderlineNode {
|
||||
/// How to stroke the line. The text color and thickness are read from the
|
||||
/// font tables if `{auto}`.
|
||||
///
|
||||
@ -35,8 +30,12 @@ impl UnderlineNode {
|
||||
/// [care],
|
||||
/// )
|
||||
/// ```
|
||||
#[property(shorthand, resolve, fold)]
|
||||
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
||||
#[settable]
|
||||
#[shorthand]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
|
||||
/// Position of the line relative to the baseline, read from the font tables
|
||||
/// if `{auto}`.
|
||||
@ -46,8 +45,10 @@ impl UnderlineNode {
|
||||
/// The Tale Of A Faraway Line I
|
||||
/// ]
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const OFFSET: Smart<Length> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub offset: Smart<Length>,
|
||||
|
||||
/// Amount that the line will be longer or shorter than its associated text.
|
||||
///
|
||||
@ -56,8 +57,10 @@ impl UnderlineNode {
|
||||
/// underline(extent: 2pt)[Chapter 1]
|
||||
/// )
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const EXTENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub extent: Length,
|
||||
|
||||
/// Whether the line skips sections in which it would collide with the
|
||||
/// glyphs.
|
||||
@ -66,23 +69,14 @@ impl UnderlineNode {
|
||||
/// This #underline(evade: true)[is great].
|
||||
/// This #underline(evade: false)[is less great].
|
||||
/// ```
|
||||
pub const EVADE: bool = true;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub evade: bool,
|
||||
}
|
||||
|
||||
impl Show for UnderlineNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.0.clone().styled(
|
||||
Ok(self.body().styled(
|
||||
TextNode::DECO,
|
||||
Decoration {
|
||||
line: DecoLine::Underline,
|
||||
@ -95,7 +89,6 @@ impl Show for UnderlineNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Overline
|
||||
/// Add a line over text.
|
||||
///
|
||||
/// ## Example
|
||||
@ -103,19 +96,15 @@ impl Show for UnderlineNode {
|
||||
/// #overline[A line over text.]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to add a line over.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct OverlineNode(pub Content);
|
||||
/// Display: Overline
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct OverlineNode {
|
||||
/// The content to add a line over.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl OverlineNode {
|
||||
/// How to stroke the line. The text color and thickness are read from the
|
||||
/// font tables if `{auto}`.
|
||||
///
|
||||
@ -127,8 +116,12 @@ impl OverlineNode {
|
||||
/// [The Forest Theme],
|
||||
/// )
|
||||
/// ```
|
||||
#[property(shorthand, resolve, fold)]
|
||||
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
||||
#[settable]
|
||||
#[shorthand]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
|
||||
/// Position of the line relative to the baseline, read from the font tables
|
||||
/// if `{auto}`.
|
||||
@ -138,8 +131,10 @@ impl OverlineNode {
|
||||
/// The Tale Of A Faraway Line II
|
||||
/// ]
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const OFFSET: Smart<Length> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub offset: Smart<Length>,
|
||||
|
||||
/// Amount that the line will be longer or shorter than its associated text.
|
||||
///
|
||||
@ -148,8 +143,10 @@ impl OverlineNode {
|
||||
/// #set underline(extent: 4pt)
|
||||
/// #overline(underline[Typography Today])
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const EXTENT: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub extent: Length,
|
||||
|
||||
/// Whether the line skips sections in which it would collide with the
|
||||
/// glyphs.
|
||||
@ -163,23 +160,14 @@ impl OverlineNode {
|
||||
/// [Temple],
|
||||
/// )
|
||||
/// ```
|
||||
pub const EVADE: bool = true;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub evade: bool,
|
||||
}
|
||||
|
||||
impl Show for OverlineNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.0.clone().styled(
|
||||
Ok(self.body().styled(
|
||||
TextNode::DECO,
|
||||
Decoration {
|
||||
line: DecoLine::Overline,
|
||||
@ -192,7 +180,6 @@ impl Show for OverlineNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Strikethrough
|
||||
/// Strike through text.
|
||||
///
|
||||
/// ## Example
|
||||
@ -200,19 +187,15 @@ impl Show for OverlineNode {
|
||||
/// This is #strike[not] relevant.
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to strike through.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct StrikeNode(pub Content);
|
||||
/// Display: Strikethrough
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct StrikeNode {
|
||||
/// The content to strike through.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl StrikeNode {
|
||||
/// How to stroke the line. The text color and thickness are read from the
|
||||
/// font tables if `{auto}`.
|
||||
///
|
||||
@ -223,8 +206,12 @@ impl StrikeNode {
|
||||
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
|
||||
/// This is #strike(stroke: 10pt)[redacted].
|
||||
/// ```
|
||||
#[property(shorthand, resolve, fold)]
|
||||
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
||||
#[settable]
|
||||
#[shorthand]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
|
||||
/// Position of the line relative to the baseline, read from the font tables
|
||||
/// if `{auto}`.
|
||||
@ -236,8 +223,10 @@ impl StrikeNode {
|
||||
/// This is #strike(offset: auto)[low-ish]. \
|
||||
/// This is #strike(offset: -3.5pt)[on-top].
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const OFFSET: Smart<Length> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub offset: Smart<Length>,
|
||||
|
||||
/// 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)[extends] beyond the word.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const EXTENT: Length = Length::zero();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default]
|
||||
pub extent: Length,
|
||||
}
|
||||
|
||||
impl Show for StrikeNode {
|
||||
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.0.clone().styled(
|
||||
Ok(self.body().styled(
|
||||
TextNode::DECO,
|
||||
Decoration {
|
||||
line: DecoLine::Strikethrough,
|
||||
@ -294,6 +274,10 @@ impl Fold for Decoration {
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Decoration: "decoration",
|
||||
}
|
||||
|
||||
/// A kind of decorative line.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum DecoLine {
|
||||
|
@ -1,24 +1,12 @@
|
||||
use super::TextNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Space
|
||||
/// A text space.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Unlabellable, Behave)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SpaceNode;
|
||||
|
||||
#[node]
|
||||
impl SpaceNode {
|
||||
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self.pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl Unlabellable for SpaceNode {}
|
||||
/// Display: Space
|
||||
/// Category: text
|
||||
#[node(Unlabellable, Behave)]
|
||||
pub struct SpaceNode {}
|
||||
|
||||
impl Behave for SpaceNode {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
@ -26,7 +14,8 @@ impl Behave for SpaceNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Line Break
|
||||
impl Unlabellable for SpaceNode {}
|
||||
|
||||
/// Inserts a line break.
|
||||
///
|
||||
/// 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
|
||||
/// break.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - justify: `bool` (named)
|
||||
/// 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
|
||||
/// ```
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Behave)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Line Break
|
||||
/// Category: text
|
||||
#[node(Behave)]
|
||||
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,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
Behaviour::Destructive
|
||||
}
|
||||
}
|
||||
|
||||
/// # Strong Emphasis
|
||||
/// Strongly emphasizes content by increasing the font weight.
|
||||
///
|
||||
/// 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
|
||||
/// function.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to strongly emphasize.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct StrongNode(pub Content);
|
||||
/// Display: Strong Emphasis
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct StrongNode {
|
||||
/// The content to strongly emphasize.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl StrongNode {
|
||||
/// The delta to apply on the font weight.
|
||||
///
|
||||
/// ```example
|
||||
/// #set strong(delta: 0)
|
||||
/// No *effect!*
|
||||
/// ```
|
||||
pub const DELTA: i64 = 300;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(300)]
|
||||
pub delta: i64,
|
||||
}
|
||||
|
||||
impl Show for StrongNode {
|
||||
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)]
|
||||
pub struct Delta(pub i64);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Delta,
|
||||
v: i64 => Self(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Delta => v.0.into()
|
||||
}
|
||||
|
||||
impl Fold for Delta {
|
||||
type Output = i64;
|
||||
|
||||
@ -160,7 +128,6 @@ impl Fold for Delta {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Emphasis
|
||||
/// Emphasizes content by setting it in italics.
|
||||
///
|
||||
/// - 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
|
||||
/// boundaries. To emphasize part of a word, you have to use the function.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The content to emphasize.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
/// Display: Emphasis
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct EmphNode {
|
||||
/// The content to emphasize.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl Show for EmphNode {
|
||||
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)]
|
||||
pub struct Toggle;
|
||||
|
||||
cast_from_value! {
|
||||
Toggle,
|
||||
_: Value => Self,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
_: Toggle => Value::None
|
||||
}
|
||||
|
||||
impl Fold for Toggle {
|
||||
type Output = bool;
|
||||
|
||||
@ -228,7 +189,6 @@ impl Fold for Toggle {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Lowercase
|
||||
/// Convert text or content to lowercase.
|
||||
///
|
||||
/// ## Example
|
||||
@ -242,14 +202,13 @@ impl Fold for Toggle {
|
||||
/// - text: `ToCase` (positional, required)
|
||||
/// The text to convert to lowercase.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
/// Display: Lowercase
|
||||
/// Category: text
|
||||
#[func]
|
||||
pub fn lower(args: &mut Args) -> SourceResult<Value> {
|
||||
case(Case::Lower, args)
|
||||
}
|
||||
|
||||
/// # Uppercase
|
||||
/// Convert text or content to uppercase.
|
||||
///
|
||||
/// ## Example
|
||||
@ -263,8 +222,8 @@ pub fn lower(args: &mut Args) -> SourceResult<Value> {
|
||||
/// - text: `ToCase` (positional, required)
|
||||
/// The text to convert to uppercase.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
/// Display: Uppercase
|
||||
/// Category: text
|
||||
#[func]
|
||||
pub fn upper(args: &mut Args) -> SourceResult<Value> {
|
||||
case(Case::Upper, args)
|
||||
@ -272,21 +231,22 @@ pub fn upper(args: &mut Args) -> SourceResult<Value> {
|
||||
|
||||
/// Change the case of text.
|
||||
fn case(case: Case, args: &mut Args) -> SourceResult<Value> {
|
||||
let Spanned { v, span } = args.expect("string or content")?;
|
||||
Ok(match v {
|
||||
Value::Str(v) => Value::Str(case.apply(&v).into()),
|
||||
Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
|
||||
v => bail!(span, "expected string or content, found {}", v.type_name()),
|
||||
Ok(match args.expect("string or content")? {
|
||||
ToCase::Str(v) => Value::Str(case.apply(&v).into()),
|
||||
ToCase::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
|
||||
})
|
||||
}
|
||||
|
||||
/// A value whose case can be changed.
|
||||
struct ToCase;
|
||||
enum ToCase {
|
||||
Str(Str),
|
||||
Content(Content),
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ToCase,
|
||||
_: Str => Self,
|
||||
_: Content => Self,
|
||||
v: Str => Self::Str(v),
|
||||
v: Content => Self::Content(v),
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
|
||||
@ -336,15 +308,14 @@ impl Case {
|
||||
/// - text: `Content` (positional, required)
|
||||
/// The text to display to small capitals.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
/// Display: Small Capitals
|
||||
/// Category: text
|
||||
#[func]
|
||||
pub fn smallcaps(args: &mut Args) -> SourceResult<Value> {
|
||||
let body: Content = args.expect("content")?;
|
||||
Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
|
||||
}
|
||||
|
||||
/// # Blind Text
|
||||
/// Create blind text.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
/// Display: Blind Text
|
||||
/// Category: text
|
||||
#[func]
|
||||
pub fn lorem(args: &mut Args) -> SourceResult<Value> {
|
||||
let words: usize = args.expect("number of words")?;
|
||||
|
@ -22,7 +22,6 @@ use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontM
|
||||
use crate::layout::ParNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Text
|
||||
/// Customize the look and layout of text in a variety of ways.
|
||||
///
|
||||
/// This function is used often, both with set rules and directly. While the set
|
||||
@ -62,26 +61,50 @@ use crate::prelude::*;
|
||||
/// - body: `Content` (positional, required)
|
||||
/// Content in which all text is styled according to the other arguments.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable]
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct TextNode(pub EcoString);
|
||||
/// Display: Text
|
||||
/// Category: text
|
||||
#[node(Construct)]
|
||||
#[set({
|
||||
if let Some(family) = args.named("family")? {
|
||||
styles.set(Self::FAMILY, family);
|
||||
} else {
|
||||
let mut count = 0;
|
||||
let mut content = false;
|
||||
for item in args.items.iter().filter(|item| item.name.is_none()) {
|
||||
if EcoString::is(&item.value) {
|
||||
count += 1;
|
||||
} else if <Content as Cast<Spanned<Value>>>::is(&item.value) {
|
||||
content = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl TextNode {
|
||||
/// Create a new packed text node.
|
||||
pub fn packed(text: impl Into<EcoString>) -> Content {
|
||||
Self(text.into()).pack()
|
||||
// Skip the final string if it's needed as the body.
|
||||
if constructor && !content && count > 0 {
|
||||
count -= 1;
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
let mut list = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
list.push(args.find()?.unwrap());
|
||||
}
|
||||
|
||||
styles.set(Self::FAMILY, FallbackList(list));
|
||||
}
|
||||
}
|
||||
}
|
||||
})]
|
||||
pub struct TextNode {
|
||||
/// The text.
|
||||
#[positional]
|
||||
#[required]
|
||||
#[skip]
|
||||
pub text: EcoString,
|
||||
|
||||
#[node]
|
||||
impl TextNode {
|
||||
/// A prioritized sequence of font families.
|
||||
#[property(skip, referenced)]
|
||||
pub const FAMILY: FallbackList =
|
||||
FallbackList(vec![FontFamily::new("Linux Libertine")]);
|
||||
#[settable]
|
||||
#[skip]
|
||||
#[default(FallbackList(vec![FontFamily::new("Linux Libertine")]))]
|
||||
pub family: FallbackList,
|
||||
|
||||
/// Whether to allow last resort font fallback when the primary font list
|
||||
/// contains no match. This lets Typst search through all available fonts
|
||||
@ -100,7 +123,9 @@ impl TextNode {
|
||||
/// #set text(fallback: false)
|
||||
/// هذا عربي
|
||||
/// ```
|
||||
pub const FALLBACK: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub fallback: bool,
|
||||
|
||||
/// The desired font style.
|
||||
///
|
||||
@ -119,7 +144,9 @@ impl TextNode {
|
||||
/// #text("Linux Libertine", style: "italic")[Italic]
|
||||
/// #text("DejaVu Sans", style: "oblique")[Oblique]
|
||||
/// ```
|
||||
pub const STYLE: FontStyle = FontStyle::Normal;
|
||||
#[settable]
|
||||
#[default(FontStyle::Normal)]
|
||||
pub style: FontStyle,
|
||||
|
||||
/// The desired thickness of the font's glyphs. Accepts an integer between
|
||||
/// `{100}` and `{900}` or one of the predefined weight names. When the
|
||||
@ -138,7 +165,9 @@ impl TextNode {
|
||||
/// #text(weight: 500)[Medium] \
|
||||
/// #text(weight: "bold")[Bold]
|
||||
/// ```
|
||||
pub const WEIGHT: FontWeight = FontWeight::REGULAR;
|
||||
#[settable]
|
||||
#[default(FontWeight::REGULAR)]
|
||||
pub weight: FontWeight,
|
||||
|
||||
/// The desired width of the glyphs. Accepts a ratio between `{50%}` and
|
||||
/// `{200%}`. When the desired weight is not available, Typst selects the
|
||||
@ -148,7 +177,9 @@ impl TextNode {
|
||||
/// #text(stretch: 75%)[Condensed] \
|
||||
/// #text(stretch: 100%)[Normal]
|
||||
/// ```
|
||||
pub const STRETCH: FontStretch = FontStretch::NORMAL;
|
||||
#[settable]
|
||||
#[default(FontStretch::NORMAL)]
|
||||
pub stretch: FontStretch,
|
||||
|
||||
/// The size of the glyphs. This value forms the basis of the `em` unit:
|
||||
/// `{1em}` is equivalent to the font size.
|
||||
@ -160,8 +191,11 @@ impl TextNode {
|
||||
/// #set text(size: 20pt)
|
||||
/// very #text(1.5em)[big] text
|
||||
/// ```
|
||||
#[property(shorthand, fold)]
|
||||
pub const SIZE: TextSize = Abs::pt(11.0);
|
||||
#[settable]
|
||||
#[shorthand]
|
||||
#[fold]
|
||||
#[default(Abs::pt(11.0))]
|
||||
pub size: TextSize,
|
||||
|
||||
/// The glyph fill color.
|
||||
///
|
||||
@ -169,8 +203,10 @@ impl TextNode {
|
||||
/// #set text(fill: red)
|
||||
/// This text is red.
|
||||
/// ```
|
||||
#[property(shorthand)]
|
||||
pub const FILL: Paint = Color::BLACK.into();
|
||||
#[shorthand]
|
||||
#[settable]
|
||||
#[default(Color::BLACK.into())]
|
||||
pub fill: Paint,
|
||||
|
||||
/// The amount of space that should be added between characters.
|
||||
///
|
||||
@ -178,8 +214,10 @@ impl TextNode {
|
||||
/// #set text(tracking: 1.5pt)
|
||||
/// Distant text.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const TRACKING: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Length::zero())]
|
||||
pub tracking: Length,
|
||||
|
||||
/// The amount of space between words.
|
||||
///
|
||||
@ -190,8 +228,10 @@ impl TextNode {
|
||||
/// #set text(spacing: 200%)
|
||||
/// Text with distant words.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const SPACING: Rel<Length> = Rel::one();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Rel::one())]
|
||||
pub spacing: Rel<Length>,
|
||||
|
||||
/// An amount to shift the text baseline by.
|
||||
///
|
||||
@ -199,8 +239,10 @@ impl TextNode {
|
||||
/// A #text(baseline: 3pt)[lowered]
|
||||
/// word.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const BASELINE: Length = Length::zero();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Length::zero())]
|
||||
pub baseline: Length,
|
||||
|
||||
/// Whether certain glyphs can hang over into the margin in justified text.
|
||||
/// This can make justification visually more pleasing.
|
||||
@ -222,7 +264,9 @@ impl TextNode {
|
||||
/// margin, making the paragraph's
|
||||
/// edge less clear.
|
||||
/// ```
|
||||
pub const OVERHANG: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub overhang: bool,
|
||||
|
||||
/// The top end of the conceptual frame around the text used for layout and
|
||||
/// positioning. This affects the size of containers that hold text.
|
||||
@ -237,7 +281,9 @@ impl TextNode {
|
||||
/// #set text(top-edge: "cap-height")
|
||||
/// #rect(fill: aqua)[Typst]
|
||||
/// ```
|
||||
pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight);
|
||||
#[settable]
|
||||
#[default(TextEdge::Metric(VerticalFontMetric::CapHeight))]
|
||||
pub top_edge: TextEdge,
|
||||
|
||||
/// The bottom end of the conceptual frame around the text used for layout
|
||||
/// and positioning. This affects the size of containers that hold text.
|
||||
@ -252,7 +298,9 @@ impl TextNode {
|
||||
/// #set text(bottom-edge: "descender")
|
||||
/// #rect(fill: aqua)[Typst]
|
||||
/// ```
|
||||
pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
|
||||
#[settable]
|
||||
#[default(TextEdge::Metric(VerticalFontMetric::Baseline))]
|
||||
pub bottom_edge: TextEdge,
|
||||
|
||||
/// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
|
||||
///
|
||||
@ -271,12 +319,16 @@ impl TextNode {
|
||||
/// = Einleitung
|
||||
/// In diesem Dokument, ...
|
||||
/// ```
|
||||
pub const LANG: Lang = Lang::ENGLISH;
|
||||
#[settable]
|
||||
#[default(Lang::ENGLISH)]
|
||||
pub lang: Lang,
|
||||
|
||||
/// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||
///
|
||||
/// This lets the text processing pipeline make more informed choices.
|
||||
pub const REGION: Option<Region> = None;
|
||||
#[settable]
|
||||
#[default(None)]
|
||||
pub region: Option<Region>,
|
||||
|
||||
/// The dominant direction for text and inline objects. Possible values are:
|
||||
///
|
||||
@ -302,8 +354,10 @@ impl TextNode {
|
||||
/// #set text(dir: rtl)
|
||||
/// هذا عربي.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto);
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(HorizontalDir(Smart::Auto))]
|
||||
pub dir: HorizontalDir,
|
||||
|
||||
/// Whether to hyphenate text to improve line breaking. When `{auto}`, text
|
||||
/// will be hyphenated if and only if justification is enabled.
|
||||
@ -322,8 +376,10 @@ impl TextNode {
|
||||
/// enabling hyphenation can
|
||||
/// improve justification.
|
||||
/// ```
|
||||
#[property(resolve)]
|
||||
pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto);
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[default(Hyphenate(Smart::Auto))]
|
||||
pub hyphenate: Hyphenate,
|
||||
|
||||
/// Whether to apply kerning.
|
||||
///
|
||||
@ -340,7 +396,9 @@ impl TextNode {
|
||||
/// #set text(kerning: false)
|
||||
/// Totally
|
||||
/// ```
|
||||
pub const KERNING: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub kerning: bool,
|
||||
|
||||
/// Whether to apply stylistic alternates.
|
||||
///
|
||||
@ -355,14 +413,18 @@ impl TextNode {
|
||||
/// #set text(alternates: true)
|
||||
/// 0, a, g, ß
|
||||
/// ```
|
||||
pub const ALTERNATES: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub alternates: bool,
|
||||
|
||||
/// Which stylistic set to apply. Font designers can categorize alternative
|
||||
/// glyphs forms into stylistic sets. As this value is highly font-specific,
|
||||
/// you need to consult your font to know which sets are available. When set
|
||||
/// to an integer between `{1}` and `{20}`, enables the corresponding
|
||||
/// OpenType font feature from `ss01`, ..., `ss20`.
|
||||
pub const STYLISTIC_SET: Option<StylisticSet> = None;
|
||||
#[settable]
|
||||
#[default(None)]
|
||||
pub stylistic_set: Option<StylisticSet>,
|
||||
|
||||
/// Whether standard ligatures are active.
|
||||
///
|
||||
@ -378,15 +440,21 @@ impl TextNode {
|
||||
/// #set text(ligatures: false)
|
||||
/// A fine ligature.
|
||||
/// ```
|
||||
pub const LIGATURES: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub ligatures: bool,
|
||||
|
||||
/// Whether ligatures that should be used sparingly are active. Setting this
|
||||
/// to `{true}` enables the OpenType `dlig` font feature.
|
||||
pub const DISCRETIONARY_LIGATURES: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub discretionary_ligatures: bool,
|
||||
|
||||
/// Whether historical ligatures are active. Setting this to `{true}`
|
||||
/// enables the OpenType `hlig` font feature.
|
||||
pub const HISTORICAL_LIGATURES: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub historical_ligatures: bool,
|
||||
|
||||
/// Which kind of numbers / figures to select. When set to `{auto}`, the
|
||||
/// default numbers for the font are used.
|
||||
@ -399,7 +467,9 @@ impl TextNode {
|
||||
/// #set text(number-type: "old-style")
|
||||
/// Number 9.
|
||||
/// ```
|
||||
pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
|
||||
#[settable]
|
||||
#[default(Smart::Auto)]
|
||||
pub number_type: Smart<NumberType>,
|
||||
|
||||
/// The width of numbers / figures. When set to `{auto}`, the default
|
||||
/// numbers for the font are used.
|
||||
@ -414,7 +484,9 @@ impl TextNode {
|
||||
/// A 12 B 34. \
|
||||
/// A 56 B 78.
|
||||
/// ```
|
||||
pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
|
||||
#[settable]
|
||||
#[default(Smart::Auto)]
|
||||
pub number_width: Smart<NumberWidth>,
|
||||
|
||||
/// Whether to have a slash through the zero glyph. Setting this to `{true}`
|
||||
/// enables the OpenType `zero` font feature.
|
||||
@ -422,7 +494,9 @@ impl TextNode {
|
||||
/// ```example
|
||||
/// 0, #text(slashed-zero: true)[0]
|
||||
/// ```
|
||||
pub const SLASHED_ZERO: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub slashed_zero: bool,
|
||||
|
||||
/// Whether to turns numbers into fractions. Setting this to `{true}`
|
||||
/// enables the OpenType `frac` font feature.
|
||||
@ -431,7 +505,9 @@ impl TextNode {
|
||||
/// 1/2 \
|
||||
/// #text(fractions: true)[1/2]
|
||||
/// ```
|
||||
pub const FRACTIONS: bool = false;
|
||||
#[settable]
|
||||
#[default(false)]
|
||||
pub fractions: bool,
|
||||
|
||||
/// Raw OpenType features to apply.
|
||||
///
|
||||
@ -445,74 +521,59 @@ impl TextNode {
|
||||
/// #set text(features: ("frac",))
|
||||
/// 1/2
|
||||
/// ```
|
||||
#[property(fold)]
|
||||
pub const FEATURES: FontFeatures = FontFeatures(vec![]);
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[default(FontFeatures(vec![]))]
|
||||
pub features: FontFeatures,
|
||||
|
||||
/// A delta to apply on the font weight.
|
||||
#[property(skip, fold)]
|
||||
pub const DELTA: Delta = 0;
|
||||
/// Whether the font style should be inverted.
|
||||
#[property(skip, fold)]
|
||||
pub const EMPH: Toggle = false;
|
||||
/// A case transformation that should be applied to the text.
|
||||
#[property(skip)]
|
||||
pub const CASE: Option<Case> = None;
|
||||
/// Whether small capital glyphs should be used. ("smcp")
|
||||
#[property(skip)]
|
||||
pub const SMALLCAPS: bool = false;
|
||||
/// Decorative lines.
|
||||
#[property(skip, fold)]
|
||||
pub const DECO: Decoration = vec![];
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default(0)]
|
||||
pub delta: Delta,
|
||||
|
||||
/// Whether the font style should be inverted.
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default(false)]
|
||||
pub emph: Toggle,
|
||||
|
||||
/// A case transformation that should be applied to the text.
|
||||
#[settable]
|
||||
#[skip]
|
||||
#[default(None)]
|
||||
pub case: Option<Case>,
|
||||
|
||||
/// Whether small capital glyphs should be used. ("smcp")
|
||||
#[settable]
|
||||
#[skip]
|
||||
#[default(false)]
|
||||
pub smallcaps: bool,
|
||||
|
||||
/// Decorative lines.
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default(vec![])]
|
||||
pub deco: Decoration,
|
||||
}
|
||||
|
||||
impl TextNode {
|
||||
/// Create a new packed text node.
|
||||
pub fn packed(text: impl Into<EcoString>) -> Content {
|
||||
Self::new(text.into()).pack()
|
||||
}
|
||||
}
|
||||
|
||||
impl Construct for TextNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
// The text constructor is special: It doesn't create a text node.
|
||||
// Instead, it leaves the passed argument structurally unchanged, but
|
||||
// styles all text in it.
|
||||
args.expect("body")
|
||||
}
|
||||
|
||||
fn set(...) {
|
||||
if let Some(family) = args.named("family")? {
|
||||
styles.set(Self::FAMILY, family);
|
||||
} else {
|
||||
let mut count = 0;
|
||||
let mut content = false;
|
||||
for item in args.items.iter().filter(|item| item.name.is_none()) {
|
||||
if EcoString::is(&item.value) {
|
||||
count += 1;
|
||||
} else if <Content as Cast<Spanned<Value>>>::is(&item.value) {
|
||||
content = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the final string if it's needed as the body.
|
||||
if constructor && !content && count > 0 {
|
||||
count -= 1;
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
let mut list = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
list.push(args.find()?.unwrap());
|
||||
}
|
||||
|
||||
styles.set(Self::FAMILY, FallbackList(list));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"text" => Some(Value::Str(self.0.clone().into())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TextNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Text({:?})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A lowercased font family like "arial".
|
||||
@ -537,21 +598,29 @@ impl Debug for FontFamily {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
FontFamily,
|
||||
string: EcoString => Self::new(&string),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: FontFamily => v.0.into()
|
||||
}
|
||||
|
||||
/// Font family fallback list.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FallbackList(pub Vec<FontFamily>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
FallbackList,
|
||||
family: FontFamily => Self(vec![family]),
|
||||
values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: FallbackList => v.0.into()
|
||||
}
|
||||
|
||||
/// The size of text.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TextSize(pub Length);
|
||||
@ -564,11 +633,15 @@ impl Fold for TextSize {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
TextSize,
|
||||
v: Length => Self(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: TextSize => v.0.into()
|
||||
}
|
||||
|
||||
/// Specifies the bottom or top edge of text.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum TextEdge {
|
||||
@ -588,34 +661,37 @@ impl TextEdge {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
TextEdge,
|
||||
v: VerticalFontMetric => Self::Metric(v),
|
||||
v: Length => Self::Length(v),
|
||||
/// The font's ascender, which typically exceeds the height of all glyphs.
|
||||
"ascender" => Self::Metric(VerticalFontMetric::Ascender),
|
||||
/// The approximate height of uppercase letters.
|
||||
"cap-height" => Self::Metric(VerticalFontMetric::CapHeight),
|
||||
/// The approximate height of non-ascending lowercase letters.
|
||||
"x-height" => Self::Metric(VerticalFontMetric::XHeight),
|
||||
/// The baseline on which the letters rest.
|
||||
"baseline" => Self::Metric(VerticalFontMetric::Baseline),
|
||||
/// The font's ascender, which typically exceeds the depth of all glyphs.
|
||||
"descender" => Self::Metric(VerticalFontMetric::Descender),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: TextEdge => match v {
|
||||
TextEdge::Metric(metric) => metric.into(),
|
||||
TextEdge::Length(length) => length.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The direction of text and inline objects in their line.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct HorizontalDir(pub Smart<Dir>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
HorizontalDir,
|
||||
_: AutoValue => Self(Smart::Auto),
|
||||
dir: Dir => match dir.axis() {
|
||||
Axis::X => Self(Smart::Custom(dir)),
|
||||
Axis::Y => Err("must be horizontal")?,
|
||||
v: Smart<Dir> => {
|
||||
if v.map_or(false, |dir| dir.axis() == Axis::Y) {
|
||||
Err("must be horizontal")?;
|
||||
}
|
||||
Self(v)
|
||||
},
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: HorizontalDir => v.0.into()
|
||||
}
|
||||
|
||||
impl Resolve for HorizontalDir {
|
||||
type Output = Dir;
|
||||
|
||||
@ -631,10 +707,13 @@ impl Resolve for HorizontalDir {
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Hyphenate(pub Smart<bool>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
Hyphenate,
|
||||
_: AutoValue => Self(Smart::Auto),
|
||||
v: bool => Self(Smart::Custom(v)),
|
||||
v: Smart<bool> => Self(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Hyphenate => v.0.into()
|
||||
}
|
||||
|
||||
impl Resolve for Hyphenate {
|
||||
@ -664,7 +743,7 @@ impl StylisticSet {
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
StylisticSet,
|
||||
v: i64 => match v {
|
||||
1 ..= 20 => Self::new(v as u8),
|
||||
@ -672,6 +751,10 @@ castable! {
|
||||
},
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: StylisticSet => v.0.into()
|
||||
}
|
||||
|
||||
/// Which kind of numbers / figures to select.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberType {
|
||||
@ -681,16 +764,23 @@ pub enum NumberType {
|
||||
OldStyle,
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
NumberType,
|
||||
/// Numbers that fit well with capital text (the OpenType `lnum`
|
||||
/// font feature).
|
||||
"lining" => Self::Lining,
|
||||
/// Numbers that fit well into a flow of upper- and lowercase text (the
|
||||
// Numbers that fit well into a flow of upper- and lowercase text (the
|
||||
/// OpenType `onum` font feature).
|
||||
"old-style" => Self::OldStyle,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: NumberType => Value::from(match v {
|
||||
NumberType::Lining => "lining",
|
||||
NumberType::OldStyle => "old-style",
|
||||
})
|
||||
}
|
||||
|
||||
/// The width of numbers / figures.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberWidth {
|
||||
@ -700,7 +790,7 @@ pub enum NumberWidth {
|
||||
Tabular,
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
NumberWidth,
|
||||
/// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
|
||||
"proportional" => Self::Proportional,
|
||||
@ -708,11 +798,18 @@ castable! {
|
||||
"tabular" => Self::Tabular,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: NumberWidth => Value::from(match v {
|
||||
NumberWidth::Proportional => "proportional",
|
||||
NumberWidth::Tabular => "tabular",
|
||||
})
|
||||
}
|
||||
|
||||
/// OpenType font features settings.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFeatures(pub Vec<(Tag, u32)>);
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
FontFeatures,
|
||||
values: Array => Self(values
|
||||
.into_iter()
|
||||
@ -731,6 +828,18 @@ castable! {
|
||||
.collect::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: FontFeatures => Value::Dict(
|
||||
v.0.into_iter()
|
||||
.map(|(tag, num)| {
|
||||
let bytes = tag.to_bytes();
|
||||
let key = std::str::from_utf8(&bytes).unwrap_or_default();
|
||||
(key.into(), num.into())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
impl Fold for FontFeatures {
|
||||
type Output = Self;
|
||||
|
||||
|
@ -2,7 +2,6 @@ use typst::syntax::is_newline;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Smart Quote
|
||||
/// A language-aware quote that reacts to its context.
|
||||
///
|
||||
/// 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
|
||||
/// (`'` and `"`). Typst automatically makes your quotes smart.
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - double: `bool` (named)
|
||||
/// Whether this should be a double quote.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SmartQuoteNode {
|
||||
pub double: bool,
|
||||
}
|
||||
|
||||
/// Display: Smart Quote
|
||||
/// Category: text
|
||||
#[node]
|
||||
impl SmartQuoteNode {
|
||||
pub struct SmartQuoteNode {
|
||||
/// Whether this should be a double quote.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
pub double: bool,
|
||||
|
||||
/// Whether smart quotes are enabled.
|
||||
///
|
||||
/// To disable smartness for a single quote, you can also escape it with a
|
||||
@ -48,19 +41,9 @@ impl SmartQuoteNode {
|
||||
///
|
||||
/// These are "dumb" quotes.
|
||||
/// ```
|
||||
pub const ENABLED: bool = true;
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// State machine for smart quote substitution.
|
||||
|
@ -9,7 +9,6 @@ use super::{
|
||||
use crate::layout::BlockNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Raw Text / Code
|
||||
/// Raw text with optional syntax highlighting.
|
||||
///
|
||||
/// Displays the text verbatim and in a monospace font. This is typically used
|
||||
@ -35,71 +34,64 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
/// ````
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - text: `EcoString` (positional, required)
|
||||
/// 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
|
||||
/// ```
|
||||
/// ````
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Raw Text / Code
|
||||
/// Category: text
|
||||
#[node(Prepare, Show, Finalize)]
|
||||
pub struct RawNode {
|
||||
/// 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,
|
||||
/// Whether the raw text is displayed as a separate block.
|
||||
pub block: bool,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl RawNode {
|
||||
/// 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"
|
||||
/// ```
|
||||
/// ````
|
||||
#[named]
|
||||
#[default(false)]
|
||||
pub block: bool,
|
||||
|
||||
/// The language to syntax-highlight in.
|
||||
///
|
||||
/// Apart from typical language tags known from Markdown, this supports the
|
||||
@ -111,24 +103,9 @@ impl RawNode {
|
||||
/// This is *Typst!*
|
||||
/// ```
|
||||
/// ````
|
||||
#[property(referenced)]
|
||||
pub const LANG: Option<EcoString> = None;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[settable]
|
||||
#[default]
|
||||
pub lang: Option<EcoString>,
|
||||
}
|
||||
|
||||
impl Prepare for RawNode {
|
||||
@ -138,19 +115,14 @@ impl Prepare for RawNode {
|
||||
mut this: Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Content> {
|
||||
this.push_field(
|
||||
"lang",
|
||||
match styles.get(Self::LANG) {
|
||||
Some(lang) => Value::Str(lang.clone().into()),
|
||||
None => Value::None,
|
||||
},
|
||||
);
|
||||
this.push_field("lang", styles.get(Self::LANG).clone());
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for RawNode {
|
||||
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 foreground = THEME
|
||||
.settings
|
||||
@ -161,8 +133,8 @@ impl Show for RawNode {
|
||||
|
||||
let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
|
||||
let root = match lang.as_deref() {
|
||||
Some("typc") => syntax::parse_code(&self.text),
|
||||
_ => syntax::parse(&self.text),
|
||||
Some("typc") => syntax::parse_code(&text),
|
||||
_ => syntax::parse(&text),
|
||||
};
|
||||
|
||||
let mut seq = vec![];
|
||||
@ -172,7 +144,7 @@ impl Show for RawNode {
|
||||
vec![],
|
||||
&highlighter,
|
||||
&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 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 {
|
||||
seq.push(LinebreakNode { justify: false }.pack());
|
||||
seq.push(LinebreakNode::new().pack());
|
||||
}
|
||||
|
||||
for (style, piece) in
|
||||
@ -196,16 +168,11 @@ impl Show for RawNode {
|
||||
|
||||
Content::sequence(seq)
|
||||
} else {
|
||||
TextNode::packed(self.text.clone())
|
||||
TextNode::packed(text)
|
||||
};
|
||||
|
||||
if self.block {
|
||||
realized = BlockNode {
|
||||
body: realized,
|
||||
width: Smart::Auto,
|
||||
height: Smart::Auto,
|
||||
}
|
||||
.pack();
|
||||
if self.block() {
|
||||
realized = BlockNode::new().with_body(realized).pack();
|
||||
}
|
||||
|
||||
Ok(realized)
|
||||
|
@ -154,7 +154,7 @@ impl<'a> ShapedText<'a> {
|
||||
for family in families(self.styles) {
|
||||
if let Some(font) = world
|
||||
.book()
|
||||
.select(family, self.variant)
|
||||
.select(family.as_str(), self.variant)
|
||||
.and_then(|id| world.font(id))
|
||||
{
|
||||
expand(&font);
|
||||
@ -209,7 +209,7 @@ impl<'a> ShapedText<'a> {
|
||||
let world = vt.world();
|
||||
let font = world
|
||||
.book()
|
||||
.select(family, self.variant)
|
||||
.select(family.as_str(), self.variant)
|
||||
.and_then(|id| world.font(id))?;
|
||||
let ttf = font.ttf();
|
||||
let glyph_id = ttf.glyph_index('-')?;
|
||||
@ -351,7 +351,7 @@ fn shape_segment<'a>(
|
||||
ctx: &mut ShapingContext,
|
||||
base: usize,
|
||||
text: &str,
|
||||
mut families: impl Iterator<Item = &'a str> + Clone,
|
||||
mut families: impl Iterator<Item = FontFamily> + Clone,
|
||||
) {
|
||||
// Fonts dont have newlines and tabs.
|
||||
if text.chars().all(|c| c == '\n' || c == '\t') {
|
||||
@ -362,7 +362,7 @@ fn shape_segment<'a>(
|
||||
let world = ctx.vt.world();
|
||||
let book = world.book();
|
||||
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))
|
||||
.filter(|font| !ctx.used.contains(font))
|
||||
});
|
||||
@ -549,7 +549,7 @@ pub fn variant(styles: StyleChain) -> FontVariant {
|
||||
}
|
||||
|
||||
/// 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] = &[
|
||||
"linux libertine",
|
||||
"twitter color emoji",
|
||||
@ -562,9 +562,8 @@ pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
|
||||
styles
|
||||
.get(TextNode::FAMILY)
|
||||
.0
|
||||
.iter()
|
||||
.map(|family| family.as_str())
|
||||
.chain(tail.iter().copied())
|
||||
.into_iter()
|
||||
.chain(tail.iter().copied().map(FontFamily::new))
|
||||
}
|
||||
|
||||
/// Collect the tags of the OpenType features to apply.
|
||||
|
@ -3,7 +3,6 @@ use typst::model::SequenceNode;
|
||||
use super::{variant, SpaceNode, TextNode, TextSize};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Subscript
|
||||
/// Set text in subscript.
|
||||
///
|
||||
/// The text is rendered smaller and its baseline is lowered.
|
||||
@ -13,19 +12,15 @@ use crate::prelude::*;
|
||||
/// Revenue#sub[yearly]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The text to display in subscript.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SubNode(pub Content);
|
||||
/// Display: Subscript
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct SubNode {
|
||||
/// The text to display in subscript.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl SubNode {
|
||||
/// Whether to prefer the dedicated subscript characters of the font.
|
||||
///
|
||||
/// 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: false)[1]
|
||||
/// ```
|
||||
pub const TYPOGRAPHIC: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub typographic: bool,
|
||||
|
||||
/// The baseline shift for synthetic subscripts. Does not apply if
|
||||
/// `typographic` is true and the font has subscript codepoints for the
|
||||
/// given `body`.
|
||||
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
|
||||
/// `typographic` is true and the font has subscript codepoints for the
|
||||
/// given `body`.
|
||||
pub const SIZE: TextSize = TextSize(Em::new(0.6).into());
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("body")?).pack())
|
||||
}
|
||||
#[settable]
|
||||
#[default(TextSize(Em::new(0.6).into()))]
|
||||
pub size: TextSize,
|
||||
}
|
||||
|
||||
impl Show for SubNode {
|
||||
@ -58,9 +57,10 @@ impl Show for SubNode {
|
||||
_: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Content> {
|
||||
let body = self.body();
|
||||
let mut transformed = None;
|
||||
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) {
|
||||
transformed = Some(TextNode::packed(text));
|
||||
}
|
||||
@ -71,12 +71,11 @@ impl Show for SubNode {
|
||||
let mut map = StyleMap::new();
|
||||
map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
|
||||
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.
|
||||
///
|
||||
/// The text is rendered smaller and its baseline is raised.
|
||||
@ -86,19 +85,15 @@ impl Show for SubNode {
|
||||
/// 1#super[st] try!
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional, required)
|
||||
/// The text to display in superscript.
|
||||
///
|
||||
/// ## Category
|
||||
/// text
|
||||
#[func]
|
||||
#[capable(Show)]
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SuperNode(pub Content);
|
||||
/// Display: Superscript
|
||||
/// Category: text
|
||||
#[node(Show)]
|
||||
pub struct SuperNode {
|
||||
/// The text to display in superscript.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
#[node]
|
||||
impl SuperNode {
|
||||
/// Whether to prefer the dedicated superscript characters of the font.
|
||||
///
|
||||
/// 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: false)[1]
|
||||
/// ```
|
||||
pub const TYPOGRAPHIC: bool = true;
|
||||
#[settable]
|
||||
#[default(true)]
|
||||
pub typographic: bool,
|
||||
|
||||
/// The baseline shift for synthetic superscripts. Does not apply if
|
||||
/// `typographic` is true and the font has superscript codepoints for the
|
||||
/// given `body`.
|
||||
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
|
||||
/// `typographic` is true and the font has superscript codepoints for the
|
||||
/// given `body`.
|
||||
pub const SIZE: TextSize = TextSize(Em::new(0.6).into());
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Self(args.expect("body")?).pack())
|
||||
}
|
||||
#[settable]
|
||||
#[default(TextSize(Em::new(0.6).into()))]
|
||||
pub size: TextSize,
|
||||
}
|
||||
|
||||
impl Show for SuperNode {
|
||||
@ -131,9 +130,10 @@ impl Show for SuperNode {
|
||||
_: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Content> {
|
||||
let body = self.body();
|
||||
let mut transformed = None;
|
||||
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) {
|
||||
transformed = Some(TextNode::packed(text));
|
||||
}
|
||||
@ -144,7 +144,7 @@ impl Show for SuperNode {
|
||||
let mut map = StyleMap::new();
|
||||
map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
|
||||
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> {
|
||||
if content.is::<SpaceNode>() {
|
||||
Some(' '.into())
|
||||
} else if let Some(text) = content.to::<TextNode>() {
|
||||
convert_script(&text.0, sub)
|
||||
} else if let Some(node) = content.to::<TextNode>() {
|
||||
convert_script(&node.text(), sub)
|
||||
} else if let Some(seq) = content.to::<SequenceNode>() {
|
||||
let mut full = EcoString::new();
|
||||
for item in seq.0.iter() {
|
||||
match search_text(item, sub) {
|
||||
for item in seq.children() {
|
||||
match search_text(&item, sub) {
|
||||
Some(text) => full.push_str(&text),
|
||||
None => return None,
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Image
|
||||
/// A raster or vector graphic.
|
||||
///
|
||||
/// Supported formats are PNG, JPEG, GIF and SVG.
|
||||
@ -18,62 +18,52 @@ use crate::prelude::*;
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - path: `EcoString` (positional, required)
|
||||
/// Path to an image file.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Image
|
||||
/// Category: visualize
|
||||
#[node(Construct, Layout)]
|
||||
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>>,
|
||||
|
||||
/// The height of the image.
|
||||
#[named]
|
||||
#[default]
|
||||
pub height: Smart<Rel<Length>>,
|
||||
|
||||
/// How the image should adjust itself to a given area.
|
||||
#[settable]
|
||||
#[default(ImageFit::Cover)]
|
||||
pub fit: ImageFit,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl ImageNode {
|
||||
/// How the image should adjust itself to a given area.
|
||||
pub const FIT: ImageFit = ImageFit::Cover;
|
||||
|
||||
impl Construct for ImageNode {
|
||||
fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let Spanned { v: path, span } =
|
||||
args.expect::<Spanned<EcoString>>("path to image file")?;
|
||||
|
||||
let full = vm.locate(&path).at(span)?;
|
||||
let buffer = vm.world().file(&full).at(span)?;
|
||||
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),
|
||||
_ => 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())
|
||||
let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into();
|
||||
let _ = load(vm.world(), &path).at(span)?;
|
||||
let width = args.named::<Smart<Rel<Length>>>("width")?.unwrap_or_default();
|
||||
let height = args.named::<Smart<Rel<Length>>>("height")?.unwrap_or_default();
|
||||
Ok(ImageNode::new(path).with_width(width).with_height(height).pack())
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for ImageNode {
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut Vt,
|
||||
vt: &mut Vt,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> 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
|
||||
.zip(regions.base())
|
||||
.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;
|
||||
|
||||
// Find out whether the image is wider or taller than the target size.
|
||||
let pxw = self.image.width() as f64;
|
||||
let pxh = self.image.height() as f64;
|
||||
let pxw = image.width() as f64;
|
||||
let pxh = image.height() as f64;
|
||||
let px_ratio = pxw / pxh;
|
||||
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
|
||||
// process.
|
||||
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);
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
@ -142,7 +132,7 @@ pub enum ImageFit {
|
||||
Stretch,
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
ImageFit,
|
||||
/// The image should completely cover the area. This is the default.
|
||||
"cover" => Self::Cover,
|
||||
@ -152,3 +142,27 @@ castable! {
|
||||
/// this means that the image will be distorted.
|
||||
"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)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Line
|
||||
/// A line from one point to another.
|
||||
///
|
||||
/// ## Example
|
||||
@ -26,20 +25,20 @@ use crate::prelude::*;
|
||||
/// The angle at which the line points away from the origin. Mutually
|
||||
/// exclusive with `end`.
|
||||
///
|
||||
/// ## Category
|
||||
/// visualize
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Line
|
||||
/// Category: visualize
|
||||
#[node(Construct, Layout)]
|
||||
pub struct LineNode {
|
||||
/// Where the line starts.
|
||||
#[named]
|
||||
#[default]
|
||||
pub start: Axes<Rel<Length>>,
|
||||
/// The offset from `start` where the line ends.
|
||||
pub delta: Axes<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl LineNode {
|
||||
/// The offset from `start` where the line ends.
|
||||
#[named]
|
||||
#[default]
|
||||
pub delta: Axes<Rel<Length>>,
|
||||
|
||||
/// How to stroke the line. This can be:
|
||||
///
|
||||
/// - A length specifying the stroke's thickness. The color is inherited,
|
||||
@ -52,12 +51,16 @@ impl LineNode {
|
||||
/// ```example
|
||||
/// #line(length: 100%, stroke: 2pt + red)
|
||||
/// ```
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: PartialStroke = PartialStroke::default();
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: PartialStroke,
|
||||
}
|
||||
|
||||
impl Construct for LineNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let start = args.named("start")?.unwrap_or_default();
|
||||
|
||||
let delta = match args.named::<Axes<Rel<Length>>>("end")? {
|
||||
Some(end) => end.zip(start).map(|(to, from)| to - from),
|
||||
None => {
|
||||
@ -71,8 +74,7 @@ impl LineNode {
|
||||
Axes::new(x, y)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { start, delta }.pack())
|
||||
Ok(Self::new().with_start(start).with_delta(delta).pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,13 +88,13 @@ impl Layout for LineNode {
|
||||
let stroke = styles.get(Self::STROKE).unwrap_or_default();
|
||||
|
||||
let origin = self
|
||||
.start
|
||||
.start()
|
||||
.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(l, b)| l.relative_to(b));
|
||||
|
||||
let delta = self
|
||||
.delta
|
||||
.delta()
|
||||
.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(l, b)| l.relative_to(b));
|
||||
|
@ -2,7 +2,6 @@ use std::f64::consts::SQRT_2;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// # Rectangle
|
||||
/// A rectangle with optional content.
|
||||
///
|
||||
/// ## Example
|
||||
@ -17,32 +16,28 @@ use crate::prelude::*;
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional)
|
||||
/// The content to place into the rectangle.
|
||||
///
|
||||
/// 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)]
|
||||
/// Display: Rectangle
|
||||
/// Category: visualize
|
||||
#[node(Layout)]
|
||||
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 width: Smart<Rel<Length>>,
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl RectNode {
|
||||
/// The rectangle's width, relative to its parent container.
|
||||
#[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.
|
||||
///
|
||||
/// When setting a fill, the default stroke disappears. To create a
|
||||
@ -51,7 +46,9 @@ impl RectNode {
|
||||
/// ```example
|
||||
/// #rect(fill: blue)
|
||||
/// ```
|
||||
pub const FILL: Option<Paint> = None;
|
||||
#[settable]
|
||||
#[default]
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the rectangle. This can be:
|
||||
///
|
||||
@ -85,8 +82,11 @@ impl RectNode {
|
||||
/// rect(stroke: 2pt + red),
|
||||
/// )
|
||||
/// ```
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
|
||||
|
||||
/// How much to round the rectangle's corners, relative to the minimum of
|
||||
/// the width and height divided by two. This can be:
|
||||
@ -122,8 +122,11 @@ impl RectNode {
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
#[property(resolve, fold)]
|
||||
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the rectangle's content.
|
||||
///
|
||||
@ -135,20 +138,19 @@ impl RectNode {
|
||||
/// ```example
|
||||
/// #rect(inset: 0pt)[Tight])
|
||||
/// ```
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the rectangle's size without affecting the layout.
|
||||
/// See the [box's documentation]($func/box.outset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let width = args.named("width")?.unwrap_or_default();
|
||||
let height = args.named("height")?.unwrap_or_default();
|
||||
let body = args.eat()?;
|
||||
Ok(Self { body, width, height }.pack())
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
}
|
||||
|
||||
impl Layout for RectNode {
|
||||
@ -163,8 +165,8 @@ impl Layout for RectNode {
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Rect,
|
||||
&self.body,
|
||||
Axes::new(self.width, self.height),
|
||||
&self.body(),
|
||||
Axes::new(self.width(), self.height()),
|
||||
styles.get(Self::FILL),
|
||||
styles.get(Self::STROKE),
|
||||
styles.get(Self::INSET),
|
||||
@ -174,7 +176,6 @@ impl Layout for RectNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Square
|
||||
/// A square with optional content.
|
||||
///
|
||||
/// ## Example
|
||||
@ -189,69 +190,77 @@ impl Layout for RectNode {
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional)
|
||||
/// 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}`.
|
||||
///
|
||||
/// - 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)]
|
||||
/// Display: Square
|
||||
/// Category: visualize
|
||||
#[node(Construct, Layout)]
|
||||
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 width: Smart<Rel<Length>>,
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl SquareNode {
|
||||
/// 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.
|
||||
#[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
|
||||
/// [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
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
|
||||
|
||||
/// How much to round the square's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the square's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
///
|
||||
/// The default value is `{5pt}`.
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the square's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
}
|
||||
|
||||
impl Construct for SquareNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
|
||||
let width = match size {
|
||||
@ -264,8 +273,12 @@ impl SquareNode {
|
||||
size => size,
|
||||
}
|
||||
.unwrap_or_default();
|
||||
let body = args.eat()?;
|
||||
Ok(Self { body, width, height }.pack())
|
||||
let body = args.eat::<Content>()?;
|
||||
Ok(Self::new()
|
||||
.with_body(body)
|
||||
.with_width(width)
|
||||
.with_height(height)
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,8 +294,8 @@ impl Layout for SquareNode {
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Square,
|
||||
&self.body,
|
||||
Axes::new(self.width, self.height),
|
||||
&self.body(),
|
||||
Axes::new(self.width(), self.height()),
|
||||
styles.get(Self::FILL),
|
||||
styles.get(Self::STROKE),
|
||||
styles.get(Self::INSET),
|
||||
@ -292,7 +305,6 @@ impl Layout for SquareNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Ellipse
|
||||
/// An ellipse with optional content.
|
||||
///
|
||||
/// ## Example
|
||||
@ -308,59 +320,59 @@ impl Layout for SquareNode {
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - body: `Content` (positional)
|
||||
/// The content to place into the ellipse.
|
||||
///
|
||||
/// 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)]
|
||||
/// Display: Ellipse
|
||||
/// Category: visualize
|
||||
#[node(Layout)]
|
||||
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 width: Smart<Rel<Length>>,
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl EllipseNode {
|
||||
/// The ellipse's width, relative to its parent container.
|
||||
#[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
|
||||
/// [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
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
|
||||
/// How much to pad the ellipse's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
///
|
||||
/// The default value is `{5pt}`.
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the ellipse's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let width = args.named("width")?.unwrap_or_default();
|
||||
let height = args.named("height")?.unwrap_or_default();
|
||||
let body = args.eat()?;
|
||||
Ok(Self { body, width, height }.pack())
|
||||
}
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
}
|
||||
|
||||
impl Layout for EllipseNode {
|
||||
@ -375,8 +387,8 @@ impl Layout for EllipseNode {
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Ellipse,
|
||||
&self.body,
|
||||
Axes::new(self.width, self.height),
|
||||
&self.body(),
|
||||
Axes::new(self.width(), self.height()),
|
||||
styles.get(Self::FILL),
|
||||
styles.get(Self::STROKE).map(Sides::splat),
|
||||
styles.get(Self::INSET),
|
||||
@ -386,7 +398,6 @@ impl Layout for EllipseNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Circle
|
||||
/// A circle with optional content.
|
||||
///
|
||||
/// ## Example
|
||||
@ -423,40 +434,68 @@ impl Layout for EllipseNode {
|
||||
/// In contrast to `size`, this can be relative to the parent container's
|
||||
/// height.
|
||||
///
|
||||
/// ## Category
|
||||
/// visualize
|
||||
#[func]
|
||||
#[capable(Layout)]
|
||||
#[derive(Debug, Hash)]
|
||||
/// Display: Circle
|
||||
/// Category: visualize
|
||||
#[node(Construct, Layout)]
|
||||
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 width: Smart<Rel<Length>>,
|
||||
pub height: Smart<Rel<Length>>,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl CircleNode {
|
||||
/// The circle's width. This is mutually exclusive with `radius` and
|
||||
/// `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
|
||||
/// [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
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto;
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Smart::Auto)]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
|
||||
/// How much to pad the circle's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
///
|
||||
/// The default value is `{5pt}`.
|
||||
#[property(resolve, fold)]
|
||||
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the circle's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
#[property(resolve, fold)]
|
||||
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
||||
#[settable]
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
}
|
||||
|
||||
impl Construct for CircleNode {
|
||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let size = args
|
||||
.named::<Smart<Length>>("radius")?
|
||||
@ -471,8 +510,12 @@ impl CircleNode {
|
||||
size => size,
|
||||
}
|
||||
.unwrap_or_default();
|
||||
let body = args.eat()?;
|
||||
Ok(Self { body, width, height }.pack())
|
||||
let body = args.eat::<Content>()?;
|
||||
Ok(Self::new()
|
||||
.with_body(body)
|
||||
.with_width(width)
|
||||
.with_height(height)
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,8 +531,8 @@ impl Layout for CircleNode {
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Circle,
|
||||
&self.body,
|
||||
Axes::new(self.width, self.height),
|
||||
&self.body(),
|
||||
Axes::new(self.width(), self.height()),
|
||||
styles.get(Self::FILL),
|
||||
styles.get(Self::STROKE).map(Sides::splat),
|
||||
styles.get(Self::INSET),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::Token;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Expand the `castable!` macro.
|
||||
pub fn castable(stream: TokenStream) -> Result<TokenStream> {
|
||||
/// Expand the `cast_from_value!` macro.
|
||||
pub fn cast_from_value(stream: TokenStream) -> Result<TokenStream> {
|
||||
let castable: Castable = syn::parse2(stream)?;
|
||||
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.
|
||||
fn create_is_func(castable: &Castable) -> TokenStream {
|
||||
let mut string_arms = vec![];
|
||||
@ -163,7 +230,7 @@ fn create_describe_func(castable: &Castable) -> TokenStream {
|
||||
|
||||
if let Some(name) = &castable.name {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,22 @@
|
||||
use unscanny::Scanner;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Expand the `#[func]` macro.
|
||||
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::Enum(item) => documentation(&item.attrs),
|
||||
syn::Item::Fn(item) => documentation(&item.attrs),
|
||||
_ => 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 category = section(&mut docs, "Category", 2).expect("missing category");
|
||||
let docs = docs.trim();
|
||||
|
||||
let info = quote! {
|
||||
::typst::eval::FuncInfo {
|
||||
name,
|
||||
display: #display,
|
||||
category: #category,
|
||||
display: "TODO",
|
||||
category: "TODO",
|
||||
docs: #docs,
|
||||
params: ::std::vec![#(#params),*],
|
||||
returns: ::std::vec![#(#returns),*]
|
||||
@ -82,7 +74,7 @@ pub fn func(item: syn::Item) -> Result<TokenStream> {
|
||||
}
|
||||
|
||||
/// 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 needle = format!("\n{hashtags} {title}\n");
|
||||
let start = docs.find(&needle)?;
|
||||
|
@ -2,32 +2,23 @@
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
/// 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)*)
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
mod capable;
|
||||
#[macro_use]
|
||||
mod util;
|
||||
mod castable;
|
||||
mod func;
|
||||
mod node;
|
||||
mod symbols;
|
||||
|
||||
use proc_macro::TokenStream as BoundaryStream;
|
||||
use proc_macro2::{TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::{parse_quote, Ident, Result};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
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.
|
||||
#[proc_macro_attribute]
|
||||
@ -38,33 +29,25 @@ pub fn func(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
||||
|
||||
/// Implement `Node` for a struct.
|
||||
#[proc_macro_attribute]
|
||||
pub fn node(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
||||
let item = syn::parse_macro_input!(item as syn::ItemImpl);
|
||||
node::node(item).unwrap_or_else(|err| err.to_compile_error()).into()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub fn node(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
||||
let item = syn::parse_macro_input!(item as syn::ItemStruct);
|
||||
node::node(stream.into(), item)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Implement `Cast` and optionally `Type` for a type.
|
||||
#[proc_macro]
|
||||
pub fn castable(stream: BoundaryStream) -> BoundaryStream {
|
||||
castable::castable(stream.into())
|
||||
pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
|
||||
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())
|
||||
.into()
|
||||
}
|
||||
@ -76,32 +59,3 @@ pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.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")
|
||||
}
|
||||
|
@ -1,309 +1,370 @@
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::Token;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Expand the `#[node]` macro.
|
||||
pub fn node(body: syn::ItemImpl) -> Result<TokenStream> {
|
||||
let node = prepare(body)?;
|
||||
create(&node)
|
||||
pub fn node(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
|
||||
let node = prepare(stream, &body)?;
|
||||
Ok(create(&node))
|
||||
}
|
||||
|
||||
/// Details about a 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>,
|
||||
vis: syn::Visibility,
|
||||
name: Ident,
|
||||
value_ty: syn::Type,
|
||||
output_ty: syn::Type,
|
||||
default: syn::Expr,
|
||||
skip: bool,
|
||||
referenced: bool,
|
||||
shorthand: Option<Shorthand>,
|
||||
resolve: bool,
|
||||
fold: bool,
|
||||
ident: Ident,
|
||||
name: String,
|
||||
capable: Vec<Ident>,
|
||||
set: Option<syn::Block>,
|
||||
fields: Vec<Field>,
|
||||
}
|
||||
|
||||
struct Field {
|
||||
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 {
|
||||
Positional,
|
||||
Named(Ident),
|
||||
}
|
||||
|
||||
/// Preprocess the impl block of a node.
|
||||
fn prepare(body: syn::ItemImpl) -> Result<Node> {
|
||||
// Extract the generic type arguments.
|
||||
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"),
|
||||
}
|
||||
impl Node {
|
||||
fn inherent(&self) -> impl Iterator<Item = &Field> + Clone {
|
||||
self.fields.iter().filter(|field| !field.settable)
|
||||
}
|
||||
|
||||
fn settable(&self) -> impl Iterator<Item = &Field> + Clone {
|
||||
self.fields.iter().filter(|field| field.settable)
|
||||
}
|
||||
}
|
||||
|
||||
/// Preprocess the node's definition.
|
||||
fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Node> {
|
||||
let syn::Fields::Named(named) = &body.fields else {
|
||||
bail!(body, "expected named fields");
|
||||
};
|
||||
|
||||
let mut fields = vec![];
|
||||
for field in &named.named {
|
||||
let Some(mut ident) = field.ident.clone() else {
|
||||
bail!(field, "expected named field");
|
||||
};
|
||||
|
||||
let mut attrs = field.attrs.clone();
|
||||
let settable = has_attr(&mut attrs, "settable");
|
||||
if settable {
|
||||
ident = Ident::new(&ident.to_string().to_uppercase(), ident.span());
|
||||
}
|
||||
|
||||
let field = Field {
|
||||
vis: field.vis.clone(),
|
||||
ident: ident.clone(),
|
||||
with_ident: Ident::new(&format!("with_{}", ident), ident.span()),
|
||||
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,
|
||||
Some(ident) => Shorthand::Named(ident),
|
||||
}),
|
||||
|
||||
settable,
|
||||
fold: has_attr(&mut attrs, "fold"),
|
||||
resolve: has_attr(&mut attrs, "resolve"),
|
||||
skip: has_attr(&mut attrs, "skip"),
|
||||
|
||||
ty: field.ty.clone(),
|
||||
default: parse_attr(&mut attrs, "default")?.map(|opt| {
|
||||
opt.unwrap_or_else(|| parse_quote! { ::std::default::Default::default() })
|
||||
}),
|
||||
|
||||
attrs: {
|
||||
validate_attrs(&attrs)?;
|
||||
attrs
|
||||
},
|
||||
};
|
||||
|
||||
if !field.positional && !field.named && !field.variadic && !field.settable {
|
||||
bail!(ident, "expected positional, named, variadic, or settable");
|
||||
}
|
||||
|
||||
if !field.required && !field.variadic && field.default.is_none() {
|
||||
bail!(ident, "non-required fields must have a default value");
|
||||
}
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
let capable = Punctuated::<Ident, Token![,]>::parse_terminated
|
||||
.parse2(stream)?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut attrs = body.attrs.clone();
|
||||
Ok(Node {
|
||||
body,
|
||||
params,
|
||||
self_ty,
|
||||
self_name,
|
||||
self_args,
|
||||
properties,
|
||||
construct,
|
||||
set,
|
||||
field,
|
||||
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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Preprocess and validate a property constant.
|
||||
fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> {
|
||||
let mut attrs = item.attrs.clone();
|
||||
let tokens = match attrs
|
||||
/// Produce the node's definition.
|
||||
fn create(node: &Node) -> TokenStream {
|
||||
let attrs = &node.attrs;
|
||||
let vis = &node.vis;
|
||||
let ident = &node.ident;
|
||||
let name = &node.name;
|
||||
let new = create_new_func(node);
|
||||
let construct = node
|
||||
.capable
|
||||
.iter()
|
||||
.position(|attr| attr.path.is_ident("property"))
|
||||
.map(|i| attrs.remove(i))
|
||||
{
|
||||
Some(attr) => attr.parse_args::<TokenStream>()?,
|
||||
None => TokenStream::default(),
|
||||
};
|
||||
.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 mut skip = false;
|
||||
let mut shorthand = None;
|
||||
let mut referenced = false;
|
||||
let mut resolve = false;
|
||||
let mut fold = false;
|
||||
let mut modules = vec![];
|
||||
let mut items = vec![];
|
||||
let scope = quote::format_ident!("__{}_keys", ident);
|
||||
|
||||
// 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;
|
||||
if let Some(TokenTree::Group(group)) = stream.peek() {
|
||||
let span = group.span();
|
||||
let string = group.to_string();
|
||||
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() {
|
||||
"skip" => skip = true,
|
||||
"shorthand" => {
|
||||
shorthand = Some(match arg {
|
||||
Some(name) => Shorthand::Named(name),
|
||||
None => Shorthand::Positional,
|
||||
});
|
||||
}
|
||||
"referenced" => referenced = true,
|
||||
"resolve" => resolve = true,
|
||||
"fold" => fold = true,
|
||||
_ => bail!(ident, "invalid attribute"),
|
||||
}
|
||||
}
|
||||
|
||||
if referenced && (fold || resolve) {
|
||||
bail!(item.ident, "referenced is mutually exclusive with fold and resolve");
|
||||
}
|
||||
|
||||
// The type of the property's value is what the user of our macro wrote as
|
||||
// type of the const, but the real type of the const will be a unique `Key`
|
||||
// type.
|
||||
let value_ty = item.ty.clone();
|
||||
let output_ty = if referenced {
|
||||
parse_quote! { &'a #value_ty }
|
||||
} else if fold && resolve {
|
||||
parse_quote! {
|
||||
<<#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 {
|
||||
attrs,
|
||||
vis: item.vis.clone(),
|
||||
name: item.ident.clone(),
|
||||
value_ty,
|
||||
output_ty,
|
||||
default: item.expr.clone(),
|
||||
skip,
|
||||
shorthand,
|
||||
referenced,
|
||||
resolve,
|
||||
fold,
|
||||
})
|
||||
}
|
||||
|
||||
/// Produce the necessary items for a type to become a node.
|
||||
fn create(node: &Node) -> Result<TokenStream> {
|
||||
let params = &node.params;
|
||||
let self_ty = &node.self_ty;
|
||||
|
||||
let id_method = create_node_id_method();
|
||||
let name_method = create_node_name_method(node);
|
||||
let construct_func = create_node_construct_func(node);
|
||||
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! {
|
||||
impl<#params> ::typst::model::Node for #self_ty {
|
||||
#id_method
|
||||
#name_method
|
||||
#construct_func
|
||||
#set_func
|
||||
#properties_func
|
||||
#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! {
|
||||
for field in node.settable() {
|
||||
let ident = &field.ident;
|
||||
let attrs = &field.attrs;
|
||||
let vis = &field.vis;
|
||||
let ty = &field.ty;
|
||||
modules.push(create_field_module(node, field));
|
||||
items.push(quote! {
|
||||
#(#attrs)*
|
||||
#vis const #name: #scope::#name::#key
|
||||
= #scope::#name::Key(::std::marker::PhantomData);
|
||||
#vis const #ident: #scope::#ident::Key<#ty>
|
||||
= #scope::#ident::Key(::std::marker::PhantomData);
|
||||
});
|
||||
}
|
||||
|
||||
let mut body = node.body.clone();
|
||||
body.items = items;
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#[::typst::eval::func]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
#[repr(transparent)]
|
||||
#vis struct #ident(::typst::model::Content);
|
||||
|
||||
Ok(quote! {
|
||||
#body
|
||||
impl #ident {
|
||||
#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 {
|
||||
use super::*;
|
||||
#node_impl
|
||||
#(#modules)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the node's id method.
|
||||
fn create_node_id_method() -> syn::ImplItemMethod {
|
||||
parse_quote! {
|
||||
fn id(&self) -> ::typst::model::NodeId {
|
||||
::typst::model::NodeId::of::<Self>()
|
||||
/// Create the `new` function for the node.
|
||||
fn create_new_func(node: &Node) -> TokenStream {
|
||||
let relevant = node.inherent().filter(|field| field.required || field.variadic);
|
||||
let params = relevant.clone().map(|field| {
|
||||
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.
|
||||
fn create_node_name_method(node: &Node) -> syn::ImplItemMethod {
|
||||
let name = node.self_name.trim_end_matches("Node").to_lowercase();
|
||||
parse_quote! {
|
||||
fn name(&self) -> &'static str {
|
||||
#name
|
||||
/// Create a builder pattern method for a field.
|
||||
fn create_builder_method(field: &Field) -> TokenStream {
|
||||
let Field { with_ident, ident, name, ty, .. } = field;
|
||||
let doc = format!("Set the [`{}`](Self::{}) field.", name, ident);
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
pub fn #with_ident(mut self, #ident: #ty) -> Self {
|
||||
Self(self.0.with_field(#name, #ident))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the node's `construct` function.
|
||||
fn create_node_construct_func(node: &Node) -> syn::ImplItemMethod {
|
||||
node.construct.clone().unwrap_or_else(|| {
|
||||
parse_quote! {
|
||||
/// Create an accessor methods for a field.
|
||||
fn create_accessor_method(field: &Field) -> TokenStream {
|
||||
let Field { attrs, vis, ident, name, ty, .. } = field;
|
||||
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(
|
||||
_: &::typst::eval::Vm,
|
||||
args: &mut ::typst::eval::Args,
|
||||
) -> ::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.
|
||||
fn create_node_set_func(node: &Node) -> syn::ImplItemMethod {
|
||||
let user = node.set.as_ref().map(|method| {
|
||||
let block = &method.block;
|
||||
/// Create a builder call for the constructor.
|
||||
fn create_construct_builder_call(field: &Field) -> TokenStream {
|
||||
let name = &field.name;
|
||||
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(()) } )()?; }
|
||||
});
|
||||
|
||||
let mut shorthands = vec![];
|
||||
let sets: Vec<_> = node
|
||||
.properties
|
||||
.iter()
|
||||
.filter(|p| !p.skip)
|
||||
.map(|property| {
|
||||
let name = &property.name;
|
||||
let string = name.to_string().replace('_', "-").to_lowercase();
|
||||
let value = match &property.shorthand {
|
||||
Some(Shorthand::Positional) => quote! { args.named_or_find(#string)? },
|
||||
.settable()
|
||||
.filter(|field| !field.skip)
|
||||
.map(|field| {
|
||||
let ident = &field.ident;
|
||||
let name = &field.name;
|
||||
let value = match &field.shorthand {
|
||||
Some(Shorthand::Positional) => quote! { args.named_or_find(#name)? },
|
||||
Some(Shorthand::Named(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();
|
||||
|
||||
@ -315,30 +376,12 @@ fn create_node_set_func(node: &Node) -> syn::ImplItemMethod {
|
||||
quote! { let #ident = args.named(#string)?; }
|
||||
});
|
||||
|
||||
parse_quote! {
|
||||
fn set(
|
||||
args: &mut ::typst::eval::Args,
|
||||
constructor: bool,
|
||||
) -> ::typst::diag::SourceResult<::typst::model::StyleMap> {
|
||||
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 infos = node.fields.iter().filter(|p| !p.skip).map(|field| {
|
||||
let name = &field.name;
|
||||
let value_ty = &field.ty;
|
||||
let shorthand = matches!(field.shorthand, Some(Shorthand::Positional));
|
||||
let docs = documentation(&field.attrs);
|
||||
let docs = docs.trim();
|
||||
|
||||
quote! {
|
||||
::typst::eval::ParamInfo {
|
||||
name: #name,
|
||||
@ -355,167 +398,142 @@ fn create_node_properties_func(node: &Node) -> syn::ImplItemMethod {
|
||||
}
|
||||
});
|
||||
|
||||
parse_quote! {
|
||||
fn properties() -> ::std::vec::Vec<::typst::eval::ParamInfo>
|
||||
where
|
||||
Self: Sized
|
||||
{
|
||||
::std::vec![#(#infos),*]
|
||||
quote! {
|
||||
impl ::typst::model::Set for #ident {
|
||||
fn set(
|
||||
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),*]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the node's `field` method.
|
||||
fn create_node_field_method(node: &Node) -> syn::ImplItemMethod {
|
||||
node.field.clone().unwrap_or_else(|| {
|
||||
parse_quote! {
|
||||
fn field(
|
||||
&self,
|
||||
_: &str,
|
||||
) -> ::std::option::Option<::typst::eval::Value> {
|
||||
None
|
||||
}
|
||||
/// Create the module for a single field.
|
||||
fn create_field_module(node: &Node, field: &Field) -> TokenStream {
|
||||
let node_ident = &node.ident;
|
||||
let ident = &field.ident;
|
||||
let name = &field.name;
|
||||
let ty = &field.ty;
|
||||
let default = &field.default;
|
||||
|
||||
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,
|
||||
})
|
||||
} else if field.resolve {
|
||||
quote! {
|
||||
::typst::model::Resolve::resolve(
|
||||
values.next().unwrap_or(#default),
|
||||
chain
|
||||
)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
|
||||
let name_const = create_property_name_const(node, property);
|
||||
let node_func = create_property_node_func(node);
|
||||
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.
|
||||
let scope = quote! {
|
||||
use super::*;
|
||||
|
||||
pub struct Key<__T, #params>(
|
||||
pub ::std::marker::PhantomData<(__T, #(#phantom_args,)*)>
|
||||
);
|
||||
|
||||
impl<#params> ::std::marker::Copy for #key {}
|
||||
impl<#params> ::std::clone::Clone for #key {
|
||||
pub struct Key<T>(pub ::std::marker::PhantomData<T>);
|
||||
impl ::std::marker::Copy for Key<#ty> {}
|
||||
impl ::std::clone::Clone for Key<#ty> {
|
||||
fn clone(&self) -> Self { *self }
|
||||
}
|
||||
|
||||
impl<#params> ::typst::model::Key for #key {
|
||||
type Value = #value_ty;
|
||||
type Output<'a> = #output_ty;
|
||||
#name_const
|
||||
#node_func
|
||||
#get_method
|
||||
}
|
||||
impl ::typst::model::Key for Key<#ty> {
|
||||
type Value = #ty;
|
||||
type Output = #output;
|
||||
|
||||
#copy_assertion
|
||||
fn id() -> ::typst::model::KeyId {
|
||||
static META: ::typst::model::KeyMeta = ::typst::model::KeyMeta {
|
||||
name: #name,
|
||||
};
|
||||
::typst::model::KeyId::from_meta(&META)
|
||||
}
|
||||
|
||||
fn node() -> ::typst::model::NodeId {
|
||||
::typst::model::NodeId::of::<#node_ident>()
|
||||
}
|
||||
|
||||
fn get(
|
||||
chain: ::typst::model::StyleChain,
|
||||
mut values: impl ::std::iter::Iterator<Item = Self::Value>,
|
||||
) -> Self::Output {
|
||||
#value
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
quote! {
|
||||
pub mod #ident { #scope }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
::typst::model::NodeId::of::<#self_ty>()
|
||||
/// Create the node's metadata vtable.
|
||||
fn create_vtable(node: &Node) -> TokenStream {
|
||||
let ident = &node.ident;
|
||||
let checks =
|
||||
node.capable
|
||||
.iter()
|
||||
.filter(|&ident| ident != "Construct")
|
||||
.map(|capability| {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the property's get method.
|
||||
fn create_property_get_method(property: &Property) -> syn::ImplItemMethod {
|
||||
let default = &property.default;
|
||||
let value_ty = &property.value_ty;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the assertion if the property's value must be copyable.
|
||||
fn create_property_copy_assertion(property: &Property) -> Option<TokenStream> {
|
||||
let value_ty = &property.value_ty;
|
||||
let must_be_copy = !property.fold && !property.resolve && !property.referenced;
|
||||
must_be_copy.then(|| {
|
||||
quote_spanned! { value_ty.span() =>
|
||||
const _: fn() -> () = || {
|
||||
fn must_be_copy_fold_resolve_or_referenced<T: ::std::marker::Copy>() {}
|
||||
must_be_copy_fold_resolve_or_referenced::<#value_ty>();
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,30 +1,44 @@
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::Token;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Expand the `symbols!` macro.
|
||||
pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
|
||||
let list: List = syn::parse2(stream)?;
|
||||
let pairs = list.0.iter().map(Symbol::expand);
|
||||
let list: Punctuated<Symbol, Token![,]> =
|
||||
Punctuated::parse_terminated.parse2(stream)?;
|
||||
let pairs = list.iter().map(|symbol| {
|
||||
let name = symbol.name.to_string();
|
||||
let kind = match &symbol.kind {
|
||||
Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), },
|
||||
Kind::Multiple(variants) => {
|
||||
let variants = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let c = &variant.c;
|
||||
quote! { (#name, #c) }
|
||||
});
|
||||
quote! {
|
||||
typst::eval::Symbol::list(&[#(#variants),*])
|
||||
}
|
||||
}
|
||||
};
|
||||
quote! { (#name, #kind) }
|
||||
});
|
||||
Ok(quote! { &[#(#pairs),*] })
|
||||
}
|
||||
|
||||
struct List(Punctuated<Symbol, Token![,]>);
|
||||
|
||||
impl Parse for List {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
Punctuated::parse_terminated(input).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
struct Symbol {
|
||||
name: syn::Ident,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
Single(syn::LitChar),
|
||||
Multiple(Punctuated<Variant, Token![,]>),
|
||||
}
|
||||
|
||||
struct Variant {
|
||||
name: String,
|
||||
c: syn::LitChar,
|
||||
}
|
||||
|
||||
impl Parse for Symbol {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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 {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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 {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut name = String::new();
|
||||
@ -94,11 +76,3 @@ impl Parse for Variant {
|
||||
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
88
macros/src/util.rs
Normal 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()
|
||||
}
|
132
src/doc.rs
132
src/doc.rs
@ -7,14 +7,14 @@ use std::sync::Arc;
|
||||
|
||||
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::geom::{
|
||||
self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric,
|
||||
Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
||||
self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length,
|
||||
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
||||
};
|
||||
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.
|
||||
#[derive(Debug, Default, Clone, Hash)]
|
||||
@ -274,7 +274,7 @@ impl Frame {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
for meta in styles.get(Meta::DATA) {
|
||||
for meta in styles.get(MetaNode::DATA) {
|
||||
if matches!(meta, Meta::Hidden) {
|
||||
self.clear();
|
||||
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.
|
||||
pub fn fill_and_stroke(
|
||||
&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.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
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.
|
||||
#[capable]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Meta {
|
||||
/// An internal or external link.
|
||||
@ -572,12 +597,16 @@ pub enum Meta {
|
||||
Hidden,
|
||||
}
|
||||
|
||||
/// Host for metadata.
|
||||
#[node]
|
||||
impl Meta {
|
||||
pub struct MetaNode {
|
||||
/// Metadata that should be attached to all elements affected by this style
|
||||
/// property.
|
||||
#[property(fold, skip)]
|
||||
pub const DATA: Vec<Meta> = vec![];
|
||||
#[settable]
|
||||
#[fold]
|
||||
#[skip]
|
||||
#[default]
|
||||
pub data: 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.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Destination {
|
||||
@ -598,6 +637,19 @@ pub enum Destination {
|
||||
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.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Location {
|
||||
@ -607,53 +659,21 @@ pub struct Location {
|
||||
pub pos: Point,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
/// Encode into a user-facing dictionary.
|
||||
pub fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"page" => Value::Int(self.page.get() as i64),
|
||||
"x" => Value::Length(self.pos.x.into()),
|
||||
"y" => Value::Length(self.pos.y.into()),
|
||||
}
|
||||
}
|
||||
cast_from_value! {
|
||||
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) }
|
||||
},
|
||||
}
|
||||
|
||||
/// Standard semantic roles.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Role {
|
||||
/// A paragraph.
|
||||
Paragraph,
|
||||
/// 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,
|
||||
cast_to_value! {
|
||||
v: Location => Value::Dict(dict! {
|
||||
"page" => Value::Int(v.page.get() as i64),
|
||||
"x" => Value::Length(v.pos.x.into()),
|
||||
"y" => Value::Length(v.pos.y.into()),
|
||||
})
|
||||
}
|
||||
|
592
src/eval/cast.rs
592
src/eval/cast.rs
@ -1,18 +1,12 @@
|
||||
pub use typst_macros::{cast_from_value, cast_to_value};
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Add;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ecow::EcoString;
|
||||
|
||||
use super::{castable, Array, Dict, Func, Regex, Str, Value};
|
||||
use super::{Array, Str, Value};
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
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 {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
fn cast(value: Value) -> StrResult<Self> {
|
||||
Ok(value)
|
||||
<Self as Cast>::error(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)
|
||||
}
|
||||
}
|
||||
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + CastInfo::Type("dictionary")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
CastInfo::Union(vec![])
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub use typst_macros::func;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
@ -44,7 +44,7 @@ pub struct LangItems {
|
||||
/// The id of the text node.
|
||||
pub text_id: NodeId,
|
||||
/// 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 `"`.
|
||||
pub smart_quote: fn(double: bool) -> Content,
|
||||
/// A paragraph break.
|
||||
|
@ -20,8 +20,6 @@ mod ops;
|
||||
mod scope;
|
||||
mod symbol;
|
||||
|
||||
pub use typst_macros::{castable, func};
|
||||
|
||||
pub use self::args::*;
|
||||
pub use self::array::*;
|
||||
pub use self::cast::*;
|
||||
|
@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Deref};
|
||||
use ecow::EcoString;
|
||||
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::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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StrPattern {
|
||||
@ -488,7 +492,7 @@ pub enum StrPattern {
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
StrPattern,
|
||||
text: Str => Self::Str(text),
|
||||
regex: Regex => Self::Regex(regex),
|
||||
@ -504,7 +508,7 @@ pub enum StrSide {
|
||||
End,
|
||||
}
|
||||
|
||||
castable! {
|
||||
cast_from_value! {
|
||||
StrSide,
|
||||
align: GenAlign => match align {
|
||||
GenAlign::Start => Self::Start,
|
||||
|
@ -4,15 +4,15 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ecow::eco_format;
|
||||
use siphasher::sip128::{Hasher128, SipHasher};
|
||||
|
||||
use super::{
|
||||
format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Module,
|
||||
Str, Symbol,
|
||||
cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func,
|
||||
Label, Module, Str, Symbol,
|
||||
};
|
||||
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};
|
||||
|
||||
/// A computational value.
|
||||
@ -122,6 +122,7 @@ impl Value {
|
||||
Self::Dict(dict) => dict.at(&field).cloned(),
|
||||
Self::Content(content) => content
|
||||
.field(&field)
|
||||
.cloned()
|
||||
.ok_or_else(|| eco_format!("unknown field `{field}`")),
|
||||
Self::Module(module) => module.get(&field).cloned(),
|
||||
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.
|
||||
#[derive(Clone, Hash)]
|
||||
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 {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn dyn_eq(&self, other: &Dynamic) -> bool;
|
||||
@ -462,6 +413,7 @@ primitive! { Args: "arguments", Args }
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::eval::{array, dict};
|
||||
use crate::geom::RgbaColor;
|
||||
|
||||
#[track_caller]
|
||||
fn test(value: impl Into<Value>, exp: &str) {
|
||||
|
@ -12,6 +12,7 @@ use std::sync::Arc;
|
||||
|
||||
use ttf_parser::GlyphId;
|
||||
|
||||
use crate::eval::{cast_from_value, cast_to_value, Value};
|
||||
use crate::geom::Em;
|
||||
use crate::util::Buffer;
|
||||
|
||||
@ -249,3 +250,27 @@ pub enum VerticalFontMetric {
|
||||
/// present and falls back to the descender from the `hhea` table otherwise.
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
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.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[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.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[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.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[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
|
||||
/// necessary.
|
||||
pub fn from_ratio(ratio: f32) -> Self {
|
||||
Self((ratio.max(0.5).min(2.0) * 1000.0) as u16)
|
||||
pub fn from_ratio(ratio: Ratio) -> Self {
|
||||
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,
|
||||
@ -184,12 +243,12 @@ impl FontStretch {
|
||||
}
|
||||
|
||||
/// The ratio between 0.5 and 2.0 corresponding to this stretch.
|
||||
pub fn to_ratio(self) -> f32 {
|
||||
self.0 as f32 / 1000.0
|
||||
pub fn to_ratio(self) -> Ratio {
|
||||
Ratio::new(self.0 as f64 / 1000.0)
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
@ -202,10 +261,19 @@ impl Default for FontStretch {
|
||||
|
||||
impl Debug for FontStretch {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -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.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum AbsUnit {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use std::any::Any;
|
||||
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
|
||||
|
||||
use super::*;
|
||||
use crate::eval::Array;
|
||||
|
||||
/// A container with a horizontal and vertical component.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
@ -272,3 +273,37 @@ impl BitAndAssign for Axes<bool> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -107,3 +107,100 @@ pub enum Corner {
|
||||
/// The bottom left corner.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -73,3 +73,7 @@ impl Debug for Dir {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Dir: "direction",
|
||||
}
|
||||
|
@ -134,3 +134,19 @@ impl Sum for Em {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,3 +124,11 @@ assign_impl!(Length += Length);
|
||||
assign_impl!(Length -= Length);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ mod ratio;
|
||||
mod rel;
|
||||
mod rounded;
|
||||
mod scalar;
|
||||
mod shape;
|
||||
mod sides;
|
||||
mod size;
|
||||
mod smart;
|
||||
@ -42,6 +43,7 @@ pub use self::ratio::*;
|
||||
pub use self::rel::*;
|
||||
pub use self::rounded::*;
|
||||
pub use self::scalar::*;
|
||||
pub use self::shape::*;
|
||||
pub use self::sides::*;
|
||||
pub use self::size::*;
|
||||
pub use self::smart::*;
|
||||
@ -55,6 +57,10 @@ use std::hash::{Hash, Hasher};
|
||||
use std::iter::Sum;
|
||||
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.
|
||||
pub trait Get<Index> {
|
||||
/// 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.
|
||||
pub trait Numeric:
|
||||
Sized
|
||||
|
@ -9,10 +9,7 @@ pub enum Paint {
|
||||
Solid(Color),
|
||||
}
|
||||
|
||||
impl<T> From<T> for Paint
|
||||
where
|
||||
T: Into<Color>,
|
||||
{
|
||||
impl<T: Into<Color>> From<T> for Paint {
|
||||
fn from(t: T) -> Self {
|
||||
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.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Color {
|
||||
@ -274,15 +280,16 @@ impl Debug for RgbaColor {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Color
|
||||
where
|
||||
T: Into<RgbaColor>,
|
||||
{
|
||||
impl<T: Into<RgbaColor>> From<T> for Color {
|
||||
fn from(rgba: T) -> Self {
|
||||
Self::Rgba(rgba.into())
|
||||
}
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: RgbaColor => Value::Color(v.into())
|
||||
}
|
||||
|
||||
/// An 8-bit CMYK color.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CmykColor {
|
||||
|
@ -199,3 +199,31 @@ impl<T: Numeric> Add<Ratio> for Rel<T> {
|
||||
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
35
src/geom/shape.rs
Normal 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) }
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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`.
|
||||
pub fn or(self, other: Smart<T>) -> Self {
|
||||
match self {
|
||||
@ -72,3 +84,50 @@ impl<T> Default for Smart<T> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
for (name, value) in dict.iter() {
|
||||
ctx.value_completion(Some(name.clone().into()), value, false, None);
|
||||
|
@ -39,14 +39,13 @@ extern crate self as typst;
|
||||
#[macro_use]
|
||||
pub mod util;
|
||||
#[macro_use]
|
||||
pub mod geom;
|
||||
#[macro_use]
|
||||
pub mod diag;
|
||||
#[macro_use]
|
||||
pub mod eval;
|
||||
pub mod doc;
|
||||
pub mod export;
|
||||
pub mod font;
|
||||
pub mod geom;
|
||||
pub mod ide;
|
||||
pub mod image;
|
||||
pub mod model;
|
||||
|
@ -1,27 +1,24 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::any::TypeId;
|
||||
use std::fmt::{self, Debug, Formatter, Write};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::{self, Sum};
|
||||
use std::ops::{Add, AddAssign};
|
||||
use std::sync::Arc;
|
||||
|
||||
use comemo::Tracked;
|
||||
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::eval::{Args, ParamInfo, Value, Vm};
|
||||
use crate::eval::{cast_from_value, Args, Cast, ParamInfo, Value, Vm};
|
||||
use crate::syntax::Span;
|
||||
use crate::util::ReadableTypeId;
|
||||
use crate::World;
|
||||
|
||||
/// Composable representation of styled content.
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct Content {
|
||||
obj: Arc<dyn Bounds>,
|
||||
id: NodeId,
|
||||
span: Option<Span>,
|
||||
fields: EcoVec<(EcoString, Value)>,
|
||||
modifiers: EcoVec<Modifier>,
|
||||
}
|
||||
|
||||
@ -30,55 +27,43 @@ pub struct Content {
|
||||
enum Modifier {
|
||||
Prepared,
|
||||
Guard(Guard),
|
||||
Label(Label),
|
||||
Field(EcoString, Value),
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn new<T: Node>() -> Self {
|
||||
Self {
|
||||
id: T::id(),
|
||||
span: None,
|
||||
fields: EcoVec::new(),
|
||||
modifiers: EcoVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create empty content.
|
||||
pub fn empty() -> Self {
|
||||
SequenceNode(vec![]).pack()
|
||||
SequenceNode::new(vec![]).pack()
|
||||
}
|
||||
|
||||
/// Create a new sequence node from multiples nodes.
|
||||
pub fn sequence(seq: Vec<Self>) -> Self {
|
||||
match seq.as_slice() {
|
||||
[_] => seq.into_iter().next().unwrap(),
|
||||
_ => SequenceNode(seq).pack(),
|
||||
_ => SequenceNode::new(seq).pack(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a span to the content.
|
||||
pub fn spanned(mut self, span: Span) -> Self {
|
||||
if let Some(styled) = self.to_mut::<StyledNode>() {
|
||||
styled.sub.span = Some(span);
|
||||
} else if let Some(styled) = self.to::<StyledNode>() {
|
||||
self = StyledNode {
|
||||
sub: styled.sub.clone().spanned(span),
|
||||
map: styled.map.clone(),
|
||||
}
|
||||
.pack();
|
||||
if let Some(styled) = self.to::<StyledNode>() {
|
||||
self = StyledNode::new(styled.sub().spanned(span), styled.map()).pack();
|
||||
}
|
||||
self.span = Some(span);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach a label to the content.
|
||||
pub fn labelled(mut self, label: Label) -> Self {
|
||||
for (i, modifier) in self.modifiers.iter().enumerate() {
|
||||
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));
|
||||
pub fn labelled(self, label: Label) -> Self {
|
||||
self.with_field("label", label)
|
||||
}
|
||||
|
||||
/// Style this content with a single style property.
|
||||
@ -87,31 +72,21 @@ impl Content {
|
||||
}
|
||||
|
||||
/// Style this content with a style entry.
|
||||
pub fn styled_with_entry(mut self, style: Style) -> Self {
|
||||
if let Some(styled) = self.to_mut::<StyledNode>() {
|
||||
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()
|
||||
}
|
||||
pub fn styled_with_entry(self, style: Style) -> Self {
|
||||
self.styled_with_map(style.into())
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
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.
|
||||
@ -139,12 +114,12 @@ impl Content {
|
||||
impl Content {
|
||||
/// The id of the contained node.
|
||||
pub fn id(&self) -> NodeId {
|
||||
(*self.obj).id()
|
||||
self.id
|
||||
}
|
||||
|
||||
/// The node's human-readable name.
|
||||
pub fn name(&self) -> &'static str {
|
||||
(*self.obj).name()
|
||||
self.id.name()
|
||||
}
|
||||
|
||||
/// The node's span.
|
||||
@ -154,72 +129,86 @@ impl Content {
|
||||
|
||||
/// The content's label.
|
||||
pub fn label(&self) -> Option<&Label> {
|
||||
self.modifiers.iter().find_map(|modifier| match modifier {
|
||||
Modifier::Label(label) => Some(label),
|
||||
match self.field("label")? {
|
||||
Value::Label(label) => Some(label),
|
||||
_ => 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,
|
||||
});
|
||||
}
|
||||
pub fn with_field(
|
||||
mut self,
|
||||
name: impl Into<EcoString>,
|
||||
value: impl Into<Value>,
|
||||
) -> Self {
|
||||
self.push_field(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
for modifier in &self.modifiers {
|
||||
if let Modifier::Field(other, value) = modifier {
|
||||
if name == other {
|
||||
return Some(value.clone());
|
||||
}
|
||||
}
|
||||
/// 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()));
|
||||
}
|
||||
}
|
||||
|
||||
self.obj.field(name)
|
||||
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`.
|
||||
pub fn is<T>(&self) -> bool
|
||||
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`.
|
||||
pub fn to<T>(&self) -> Option<&T>
|
||||
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.
|
||||
pub fn has<C>(&self) -> bool
|
||||
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.
|
||||
pub fn with<C>(&self) -> Option<&C>
|
||||
where
|
||||
C: Capability + ?Sized,
|
||||
C: ?Sized + 'static,
|
||||
{
|
||||
let node: &dyn Bounds = &*self.obj;
|
||||
let vtable = node.vtable(TypeId::of::<C>())?;
|
||||
let data = node as *const dyn Bounds as *const ();
|
||||
let vtable = (self.id.0.vtable)(TypeId::of::<C>())?;
|
||||
let data = self as *const Self as *const ();
|
||||
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.
|
||||
#[doc(hidden)]
|
||||
pub fn guarded(mut self, id: Guard) -> Self {
|
||||
@ -262,12 +251,40 @@ impl Content {
|
||||
pub(super) fn copy_modifiers(&mut self, from: &Content) {
|
||||
self.span = from.span;
|
||||
self.modifiers = from.modifiers.clone();
|
||||
if let Some(label) = from.label() {
|
||||
self.push_field("label", label.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Content {
|
||||
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 {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, mut rhs: Self) -> Self::Output {
|
||||
let mut 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;
|
||||
}
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
let lhs = self;
|
||||
let seq = match (lhs.to::<SequenceNode>(), rhs.to::<SequenceNode>()) {
|
||||
(Some(lhs), Some(rhs)) => lhs.0.iter().chain(&rhs.0).cloned().collect(),
|
||||
(Some(lhs), None) => lhs.0.iter().cloned().chain(iter::once(rhs)).collect(),
|
||||
(None, Some(rhs)) => iter::once(lhs).chain(rhs.0.iter().cloned()).collect(),
|
||||
(Some(lhs), Some(rhs)) => {
|
||||
lhs.children().into_iter().chain(rhs.children()).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],
|
||||
};
|
||||
|
||||
SequenceNode(seq).pack()
|
||||
SequenceNode::new(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.
|
||||
#[capable]
|
||||
#[derive(Clone, Hash)]
|
||||
#[node]
|
||||
pub struct StyledNode {
|
||||
/// The styled content.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub sub: Content,
|
||||
|
||||
/// The styles.
|
||||
#[positional]
|
||||
#[required]
|
||||
pub map: StyleMap,
|
||||
}
|
||||
|
||||
#[node]
|
||||
impl StyledNode {}
|
||||
|
||||
impl Debug for StyledNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.map.fmt(f)?;
|
||||
self.sub.fmt(f)
|
||||
}
|
||||
cast_from_value! {
|
||||
StyleMap: "style map",
|
||||
}
|
||||
|
||||
/// A sequence of nodes.
|
||||
///
|
||||
/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in
|
||||
/// Typst, the two text nodes are combined into a single sequence node.
|
||||
#[capable]
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct SequenceNode(pub Vec<Content>);
|
||||
|
||||
#[node]
|
||||
impl SequenceNode {}
|
||||
|
||||
impl Debug for SequenceNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_list().entries(self.0.iter()).finish()
|
||||
}
|
||||
pub struct SequenceNode {
|
||||
#[variadic]
|
||||
#[required]
|
||||
pub children: Vec<Content>,
|
||||
}
|
||||
|
||||
/// A label for a node.
|
||||
@ -396,80 +365,83 @@ impl Debug for Label {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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>;
|
||||
fn pack(self) -> Content;
|
||||
}
|
||||
|
||||
/// A unique identifier for a node type.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct NodeId(ReadableTypeId);
|
||||
/// A unique identifier for a node.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct NodeId(&'static NodeMeta);
|
||||
|
||||
impl NodeId {
|
||||
/// The id of the given node type.
|
||||
pub fn of<T: 'static>() -> Self {
|
||||
Self(ReadableTypeId::of::<T>())
|
||||
pub fn of<T: Node>() -> Self {
|
||||
T::id()
|
||||
}
|
||||
|
||||
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 {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
f.pad(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
/// A capability a node can have.
|
||||
///
|
||||
/// Should be implemented by trait objects that are accessible through
|
||||
/// [`Capable`].
|
||||
pub trait Capability: 'static {}
|
||||
impl Hash for NodeId {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_usize(self.0 as *const _ as usize);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamically access a trait implementation at runtime.
|
||||
pub unsafe trait Capable {
|
||||
/// Return the vtable pointer of the trait object with given type `id`
|
||||
/// if `self` implements the trait.
|
||||
fn vtable(&self, of: TypeId) -> Option<*const ()>;
|
||||
impl Eq for NodeId {}
|
||||
|
||||
impl PartialEq for NodeId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
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.
|
||||
#[capability]
|
||||
pub trait Unlabellable {}
|
||||
|
||||
#[cold]
|
||||
#[track_caller]
|
||||
fn field_is_missing(name: &str) -> ! {
|
||||
panic!("required field `{name}` is missing")
|
||||
}
|
||||
|
@ -13,4 +13,4 @@ pub use self::typeset::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use once_cell;
|
||||
pub use typst_macros::{capability, capable, node};
|
||||
pub use typst_macros::node;
|
||||
|
@ -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;
|
||||
|
||||
/// 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 cursor = 0;
|
||||
|
||||
for m in regex.find_iter(text) {
|
||||
for m in regex.find_iter(&text) {
|
||||
let start = m.start();
|
||||
if cursor < start {
|
||||
result.push(make(text[cursor..start].into()));
|
||||
@ -133,7 +133,6 @@ fn try_apply(
|
||||
}
|
||||
|
||||
/// Preparations before execution of any show rule.
|
||||
#[capability]
|
||||
pub trait Prepare {
|
||||
/// Prepare the node for show rule application.
|
||||
fn prepare(
|
||||
@ -145,7 +144,6 @@ pub trait Prepare {
|
||||
}
|
||||
|
||||
/// The base recipe for a node.
|
||||
#[capability]
|
||||
pub trait Show {
|
||||
/// Execute the base recipe for this node.
|
||||
fn show(
|
||||
@ -157,7 +155,6 @@ pub trait Show {
|
||||
}
|
||||
|
||||
/// Post-process a node after it was realized.
|
||||
#[capability]
|
||||
pub trait Finalize {
|
||||
/// 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
|
||||
|
@ -1,21 +1,16 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter;
|
||||
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::eval::{Args, Dict, Func, Regex, Value};
|
||||
use crate::geom::{
|
||||
Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
|
||||
Smart,
|
||||
};
|
||||
use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value};
|
||||
use crate::syntax::Span;
|
||||
use crate::util::ReadableTypeId;
|
||||
use crate::World;
|
||||
|
||||
/// A map of style properties.
|
||||
@ -76,7 +71,7 @@ impl StyleMap {
|
||||
/// Mark all contained properties as _scoped_. This means that they only
|
||||
/// apply to the first descendant node (of their type) in the hierarchy and
|
||||
/// not its children, too. This is used by
|
||||
/// [constructors](super::Node::construct).
|
||||
/// [constructors](super::Construct::construct).
|
||||
pub fn scoped(mut self) -> Self {
|
||||
for entry in &mut self.0 {
|
||||
if let Style::Property(property) = entry {
|
||||
@ -98,7 +93,7 @@ impl StyleMap {
|
||||
|
||||
/// Returns `Some(_)` with an optional span if this map contains styles for
|
||||
/// 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>();
|
||||
self.0.iter().find_map(|entry| match entry {
|
||||
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 {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
for entry in self.0.iter() {
|
||||
@ -154,13 +155,11 @@ impl Style {
|
||||
|
||||
impl Debug for Style {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("#[")?;
|
||||
match self {
|
||||
Self::Property(property) => property.fmt(f)?,
|
||||
Self::Recipe(recipe) => recipe.fmt(f)?,
|
||||
Self::Barrier(id) => write!(f, "Barrier for {id:?}")?,
|
||||
Self::Property(property) => property.fmt(f),
|
||||
Self::Recipe(recipe) => recipe.fmt(f),
|
||||
Self::Barrier(id) => write!(f, "#[Barrier for {id:?}]"),
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,12 +174,9 @@ pub struct Property {
|
||||
/// hierarchy. Used by constructors.
|
||||
scoped: bool,
|
||||
/// The property's value.
|
||||
value: Arc<Prehashed<dyn Bounds>>,
|
||||
value: Value,
|
||||
/// The span of the set rule the property stems from.
|
||||
origin: Option<Span>,
|
||||
/// The name of the property.
|
||||
#[cfg(debug_assertions)]
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl Property {
|
||||
@ -189,11 +185,9 @@ impl Property {
|
||||
Self {
|
||||
key: KeyId::of::<K>(),
|
||||
node: K::node(),
|
||||
value: Arc::new(Prehashed::new(value)),
|
||||
value: value.into(),
|
||||
scoped: false,
|
||||
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.
|
||||
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>() {
|
||||
(**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 {
|
||||
None
|
||||
}
|
||||
@ -234,9 +231,7 @@ impl Property {
|
||||
|
||||
impl Debug for Property {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "{} = ", self.name)?;
|
||||
write!(f, "{:?}", self.value)?;
|
||||
write!(f, "#set {}({}: {:?})", self.node.name(), self.key.name(), self.value)?;
|
||||
if self.scoped {
|
||||
write!(f, " [scoped]")?;
|
||||
}
|
||||
@ -267,47 +262,69 @@ where
|
||||
|
||||
/// A style property key.
|
||||
///
|
||||
/// This trait is not intended to be implemented manually, but rather through
|
||||
/// the `#[node]` proc-macro.
|
||||
/// This trait is not intended to be implemented manually.
|
||||
pub trait Key: Copy + 'static {
|
||||
/// 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
|
||||
/// from a style chain.
|
||||
type Output<'a>;
|
||||
type Output;
|
||||
|
||||
/// The name of the property, used for debug printing.
|
||||
const NAME: &'static str;
|
||||
/// The id of the property.
|
||||
fn id() -> KeyId;
|
||||
|
||||
/// The id of the node the key belongs to.
|
||||
fn node() -> NodeId;
|
||||
|
||||
/// Compute an output value from a sequence of values belonging to this key,
|
||||
/// folding if necessary.
|
||||
fn get<'a>(
|
||||
chain: StyleChain<'a>,
|
||||
values: impl Iterator<Item = &'a Self::Value>,
|
||||
) -> Self::Output<'a>;
|
||||
fn get(chain: StyleChain, values: impl Iterator<Item = Self::Value>) -> Self::Output;
|
||||
}
|
||||
|
||||
/// A unique identifier for a property key.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
struct KeyId(ReadableTypeId);
|
||||
/// A unique identifier for a style key.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct KeyId(&'static KeyMeta);
|
||||
|
||||
impl KeyId {
|
||||
/// The id of the given key.
|
||||
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 {
|
||||
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.
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct Recipe {
|
||||
@ -362,7 +379,7 @@ impl Recipe {
|
||||
|
||||
impl Debug for Recipe {
|
||||
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 {
|
||||
/// Define a simple node selector.
|
||||
pub fn node<T: 'static>() -> Self {
|
||||
pub fn node<T: Node>() -> Self {
|
||||
Self::Node(NodeId::of::<T>(), None)
|
||||
}
|
||||
|
||||
@ -399,17 +416,25 @@ impl Selector {
|
||||
&& 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::Regex(regex) => {
|
||||
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.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Transform {
|
||||
@ -421,6 +446,17 @@ pub enum Transform {
|
||||
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 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
|
||||
/// entry for it. Also takes care of resolving and folding and returns
|
||||
/// 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))
|
||||
}
|
||||
|
||||
@ -534,10 +570,7 @@ impl Debug for StyleChain<'_> {
|
||||
|
||||
impl PartialEq for StyleChain<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let as_ptr = |s| s as *const _;
|
||||
self.head.as_ptr() == other.head.as_ptr()
|
||||
&& self.head.len() == other.head.len()
|
||||
&& self.tail.map(as_ptr) == other.tail.map(as_ptr)
|
||||
crate::util::hash128(self) == crate::util::hash128(other)
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,13 +618,14 @@ struct 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> {
|
||||
for entry in &mut self.entries {
|
||||
match entry {
|
||||
Style::Property(property) => {
|
||||
if let Some(value) = property.downcast::<K>() {
|
||||
if let Some(value) = property.cast::<K>() {
|
||||
if !property.scoped() || self.barriers <= 1 {
|
||||
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> {
|
||||
fn default() -> Self {
|
||||
Self { items: vec![], maps: vec![] }
|
||||
@ -791,26 +839,6 @@ pub trait Resolve {
|
||||
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> {
|
||||
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.
|
||||
pub trait Fold {
|
||||
/// The type of the folded output.
|
||||
@ -907,92 +867,3 @@ where
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ use comemo::{Track, Tracked, TrackedMut};
|
||||
use super::{Content, Selector, StyleChain};
|
||||
use crate::diag::SourceResult;
|
||||
use crate::doc::{Document, Element, Frame, Location, Meta};
|
||||
use crate::eval::Value;
|
||||
use crate::geom::Transform;
|
||||
use crate::util::hash128;
|
||||
use crate::World;
|
||||
@ -162,7 +161,7 @@ impl Introspector {
|
||||
let pos = pos.transform(ts);
|
||||
let mut node = content.clone();
|
||||
let loc = Location { page, pos };
|
||||
node.push_field("loc", Value::Dict(loc.encode()));
|
||||
node.push_field("loc", loc);
|
||||
self.nodes.push((id, node));
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +60,7 @@ pub trait ArcExt<T> {
|
||||
fn take(self) -> T;
|
||||
}
|
||||
|
||||
impl<T> ArcExt<T> for Arc<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
impl<T: Clone> ArcExt<T> for Arc<T> {
|
||||
fn take(self) -> T {
|
||||
match Arc::try_unwrap(self) {
|
||||
Ok(v) => v,
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -146,9 +146,8 @@ impl Args {
|
||||
}
|
||||
|
||||
fn library() -> Library {
|
||||
/// # Test
|
||||
/// ## Category
|
||||
/// test
|
||||
/// Category: test
|
||||
/// Display: Test
|
||||
#[func]
|
||||
fn test(args: &mut typst::eval::Args) -> SourceResult<Value> {
|
||||
let lhs = args.expect::<Value>("left-hand side")?;
|
||||
@ -159,9 +158,8 @@ fn library() -> Library {
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
/// # Print
|
||||
/// ## Category
|
||||
/// test
|
||||
/// Category: test
|
||||
/// Display: Print
|
||||
#[func]
|
||||
fn print(args: &mut typst::eval::Args) -> SourceResult<Value> {
|
||||
print!("> ");
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
// Override lists.
|
||||
#show list: it => "(" + it.items.join(", ") + ")"
|
||||
#show list: it => "(" + it.items.map(item => item.body).join(", ") + ")"
|
||||
|
||||
- A
|
||||
- B
|
||||
|
@ -35,7 +35,7 @@
|
||||
#show terms: it => table(
|
||||
columns: 2,
|
||||
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
|
||||
|
@ -17,7 +17,7 @@ $ x = (-b plus.minus sqrt(b^2 - 4a c))/(2a) $
|
||||
$ binom(circle, square) $
|
||||
|
||||
---
|
||||
// Error: 8-13 missing argument: lower index
|
||||
// Error: 8-13 missing argument: lower
|
||||
$ binom(x^2) $
|
||||
|
||||
---
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user