summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-07 15:17:13 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-07 15:17:13 +0100
commit25b5bd117529cd04bb789e1988eb3a3db8025a0e (patch)
tree2fbb4650903123da047a1f1f11a0abda95286e12
parent6ab7760822ccd24b4ef126d4737d41f1be15fe19 (diff)
Fully untyped model
-rw-r--r--library/src/compute/calc.rs120
-rw-r--r--library/src/compute/construct.rs62
-rw-r--r--library/src/compute/data.rs22
-rw-r--r--library/src/compute/foundations.rs25
-rw-r--r--library/src/compute/mod.rs3
-rw-r--r--library/src/layout/align.rs109
-rw-r--r--library/src/layout/columns.rs79
-rw-r--r--library/src/layout/container.rs347
-rw-r--r--library/src/layout/enum.rs175
-rw-r--r--library/src/layout/flow.rs55
-rw-r--r--library/src/layout/grid.rs107
-rw-r--r--library/src/layout/hide.rs27
-rw-r--r--library/src/layout/list.rs191
-rw-r--r--library/src/layout/mod.rs165
-rw-r--r--library/src/layout/pad.rs66
-rw-r--r--library/src/layout/page.rs171
-rw-r--r--library/src/layout/par.rs155
-rw-r--r--library/src/layout/place.rs85
-rw-r--r--library/src/layout/repeat.rs29
-rw-r--r--library/src/layout/spacing.rs161
-rw-r--r--library/src/layout/stack.rs84
-rw-r--r--library/src/layout/table.rs151
-rw-r--r--library/src/layout/terms.rs174
-rw-r--r--library/src/layout/transform.rs170
-rw-r--r--library/src/lib.rs46
-rw-r--r--library/src/math/accent.rs118
-rw-r--r--library/src/math/align.rs18
-rw-r--r--library/src/math/attach.rs113
-rw-r--r--library/src/math/delimited.rs68
-rw-r--r--library/src/math/frac.rs71
-rw-r--r--library/src/math/matrix.rs120
-rw-r--r--library/src/math/mod.rs68
-rw-r--r--library/src/math/op.rs52
-rw-r--r--library/src/math/root.rs65
-rw-r--r--library/src/math/row.rs7
-rw-r--r--library/src/math/spacing.rs8
-rw-r--r--library/src/math/style.rs243
-rw-r--r--library/src/math/underover.rs182
-rw-r--r--library/src/meta/document.rs65
-rw-r--r--library/src/meta/heading.rs82
-rw-r--r--library/src/meta/link.rs62
-rw-r--r--library/src/meta/numbering.rs66
-rw-r--r--library/src/meta/outline.rs89
-rw-r--r--library/src/meta/reference.rs34
-rw-r--r--library/src/prelude.rs10
-rw-r--r--library/src/shared/behave.rs5
-rw-r--r--library/src/shared/ext.rs94
-rw-r--r--library/src/text/deco.rs180
-rw-r--r--library/src/text/misc.rs209
-rw-r--r--library/src/text/mod.rs373
-rw-r--r--library/src/text/quotes.rs35
-rw-r--r--library/src/text/raw.rs163
-rw-r--r--library/src/text/shaping.rs15
-rw-r--r--library/src/text/shift.rs100
-rw-r--r--library/src/visualize/image.rs100
-rw-r--r--library/src/visualize/line.rs34
-rw-r--r--library/src/visualize/shape.rs331
-rw-r--r--macros/src/capable.rs48
-rw-r--r--macros/src/castable.rs135
-rw-r--r--macros/src/func.rs16
-rw-r--r--macros/src/lib.rs88
-rw-r--r--macros/src/node.rs796
-rw-r--r--macros/src/symbols.rs84
-rw-r--r--macros/src/util.rs88
-rw-r--r--src/doc.rs132
-rw-r--r--src/eval/cast.rs474
-rw-r--r--src/eval/func.rs2
-rw-r--r--src/eval/library.rs2
-rw-r--r--src/eval/mod.rs2
-rw-r--r--src/eval/str.rs10
-rw-r--r--src/eval/value.rs68
-rw-r--r--src/font/mod.rs25
-rw-r--r--src/font/variant.rs80
-rw-r--r--src/geom/abs.rs4
-rw-r--r--src/geom/align.rs48
-rw-r--r--src/geom/axes.rs35
-rw-r--r--src/geom/corners.rs97
-rw-r--r--src/geom/dir.rs4
-rw-r--r--src/geom/em.rs16
-rw-r--r--src/geom/length.rs8
-rw-r--r--src/geom/mod.rs40
-rw-r--r--src/geom/paint.rs23
-rw-r--r--src/geom/rel.rs28
-rw-r--r--src/geom/shape.rs35
-rw-r--r--src/geom/sides.rs85
-rw-r--r--src/geom/smart.rs59
-rw-r--r--src/geom/stroke.rs34
-rw-r--r--src/ide/complete.rs5
-rw-r--r--src/lib.rs3
-rw-r--r--src/model/content.rs412
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/realize.rs7
-rw-r--r--src/model/styles.rs337
-rw-r--r--src/model/typeset.rs3
-rw-r--r--src/util/mod.rs5
-rw-r--r--tests/ref/text/raw-code.pngbin38199 -> 38186 bytes
-rw-r--r--tests/src/tests.rs10
-rw-r--r--tests/typ/compiler/show-node.typ2
-rw-r--r--tests/typ/layout/terms.typ2
-rw-r--r--tests/typ/math/frac.typ2
-rw-r--r--tests/typ/text/edge.typ4
101 files changed, 4504 insertions, 4815 deletions
diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs
index 9ebce84c..d4a4bcf6 100644
--- a/library/src/compute/calc.rs
+++ b/library/src/compute/calc.rs
@@ -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),
diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs
index 8355e20f..db442327 100644
--- a/library/src/compute/construct.rs
+++ b/library/src/compute/construct.rs
@@ -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")?;
diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs
index c604be11..90d72ade 100644
--- a/library/src/compute/data.rs
+++ b/library/src/compute/data.rs
@@ -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 } =
diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs
index 1619fb60..710ec68e 100644
--- a/library/src/compute/foundations.rs
+++ b/library/src/compute/foundations.rs
@@ -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")?;
diff --git a/library/src/compute/mod.rs b/library/src/compute/mod.rs
index cf0486db..3f6a79fc 100644
--- a/library/src/compute/mod.rs
+++ b/library/src/compute/mod.rs
@@ -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::*;
diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs
index b84ccfdc..96c0ae3b 100644
--- a/library/src/layout/align.rs
+++ b/library/src/layout/align.rs
@@ -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 {}
-
-#[node]
-impl AlignNode {
- /// The alignment.
- #[property(fold, skip)]
- pub const ALIGNS: Axes<Option<GenAlign>> =
- Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top));
+/// 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,
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- args.expect("body")
- }
+ /// 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 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())
}
}
diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs
index 0353e077..94c04509 100644
--- a/library/src/layout/columns.rs
+++ b/library/src/layout/columns.rs
@@ -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
diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs
index 8b10f7a6..67504ca3 100644
--- a/library/src/layout/container.rs
+++ b/library/src/layout/container.rs
@@ -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.
+
+ /// 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 box's height.
+
+ /// The height of the box.
+ #[named]
+ #[default]
pub height: Smart<Rel<Length>>,
-}
-#[node]
-impl BoxNode {
/// 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.
+
+ /// 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 box's height.
+
+ /// 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>>,
-}
-#[node]
-impl BlockNode {
/// 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());
-
- /// 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 and the following block.
- #[property(skip)]
- pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
+ #[settable]
+ #[resolve]
+ #[fold]
+ #[default]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// 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 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),
+ }
+}
diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs
index 53fc3327..990c0fb9 100644
--- a/library/src/layout/enum.rs
+++ b/library/src/layout/enum.rs
@@ -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 {
+ /// 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,
- /// The individual numbered items.
- pub items: StyleVec<(Option<NonZeroUsize>, Content)>,
-}
-#[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>;
diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs
index ee845f06..ea31752b 100644
--- a/library/src/layout/flow.rs
+++ b/library/src/layout/flow.rs
@@ -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);
diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs
index d0df8794..d3758fd6 100644
--- a/library/src/layout/grid.rs
+++ b/library/src/layout/grid.rs
@@ -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.
diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs
index 019dd2a6..5ba7dea4 100644
--- a/library/src/layout/hide.rs
+++ b/library/src/layout/hide.rs
@@ -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]))
}
}
diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs
index e83b91ab..0ca5ccf6 100644
--- a/library/src/layout/list.rs
+++ b/library/src/layout/list.rs
@@ -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.
+ /// 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,
- /// The individual bulleted or numbered items.
- pub items: StyleVec<Content>,
-}
-#[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
}
}
diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs
index 9ee77a61..afdfd795 100644
--- a/library/src/layout/mod.rs
+++ b/library/src/layout/mod.rs
@@ -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 {}
diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs
index 4fc2ff29..05aafc76 100644
--- a/library/src/layout/pad.rs
+++ b/library/src/layout/pad.rs
@@ -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
diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs
index 022619d7..5d1d530d 100644
--- a/library/src/layout/page.rs
+++ b/library/src/layout/page.rs
@@ -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,28 +346,17 @@ 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)]
+/// Display: Page Break
+/// Category: layout
+#[node]
pub struct PagebreakNode {
+ /// If `{true}`, the page break is skipped if the current page is already
+ /// empty.
+ #[named]
+ #[default(false)]
pub weak: bool,
}
-#[node]
-impl PagebreakNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { weak }.pack())
- }
-}
-
/// A header, footer, foreground or background definition.
#[derive(Debug, Clone, Hash)]
pub enum Marginal {
@@ -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.
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index 64a6c513..1b554d62 100644
--- a/library/src/layout/par.rs
+++ b/library/src/layout/par.rs
@@ -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>);
-
-#[node]
-impl ParNode {
+/// Display: Paragraph
+/// Category: layout
+#[node(Construct)]
+pub struct ParNode {
+ /// The paragraph's children.
+ #[variadic]
+ pub children: Vec<Content>,
+
/// 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(&quotes, node.double, peeked));
+ full.push_str(quoter.quote(&quotes, 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))
}
diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs
index 05de369b..b4aaf73d 100644
--- a/library/src/layout/place.rs
+++ b/library/src/layout/place.rs
@@ -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()
}
}
diff --git a/library/src/layout/repeat.rs b/library/src/layout/repeat.rs
index ec582c28..67dca285 100644
--- a/library/src/layout/repeat.rs
+++ b/library/src/layout/repeat.rs
@@ -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();
diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs
index a94e48da..94517ad5 100644
--- a/library/src/layout/spacing.rs
+++ b/library/src/layout/spacing.rs
@@ -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`].
- 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 }
- }
+ /// 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,
}
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),
+ }
+}
diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs
index 864b7b42..430af715 100644
--- a/library/src/layout/stack.rs
+++ b/library/src/layout/stack.rs
@@ -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);
diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs
index 3084c3d4..33ce9088 100644
--- a/library/src/layout/table.rs
+++ b/library/src/layout/table.rs
@@ -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(),
+ }
+ }
+}
diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs
index f2902b80..33b59d4d 100644
--- a/library/src/layout/terms.rs
+++ b/library/src/layout/terms.rs
@@ -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.
+ /// 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,
- /// The individual bulleted or numbered items.
- pub items: StyleVec<TermItem>,
-}
-#[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")?,
}
diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs
index 5d358e66..2ab9e5e0 100644
--- a/library/src/layout/transform.rs
+++ b/library/src/layout/transform.rs
@@ -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.
+
+ /// The content to rotate.
+ #[positional]
+ #[required]
pub body: Content,
-}
-#[node]
-impl RotateNode {
/// 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))
diff --git a/library/src/lib.rs b/library/src/lib.rs
index 0759f73f..e0994f25 100644
--- a/library/src/lib.rs
+++ b/library/src/lib.rs
@@ -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(),
}
}
diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs
index 9c474eee..164247de 100644
--- a/library/src/math/accent.rs
+++ b/library/src/math/accent.rs
@@ -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>&DiacriticalGrave;</code> |
-/// | Acute | `acute` | `´` |
-/// | Circumflex | `hat` | `^` |
-/// | Tilde | `tilde` | `~` |
-/// | Macron | `macron` | `¯` |
-/// | Breve | `breve` | `˘` |
-/// | Dot | `dot` | `.` |
-/// | Diaeresis | `diaer` | `¨` |
-/// | Circle | `circle` | `∘` |
-/// | Double acute | `acute.double` | `˝` |
-/// | Caron | `caron` | `ˇ` |
-/// | Right arrow | `arrow`, `->` | `→` |
-/// | Left arrow | `arrow.l`, `<-` | `←` |
-///
-/// ## Category
-/// math
-#[func]
-#[capable(LayoutMath)]
-#[derive(Debug, Hash)]
+/// 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>&DiacriticalGrave;</code> |
+ /// | Acute | `acute` | `´` |
+ /// | Circumflex | `hat` | `^` |
+ /// | Tilde | `tilde` | `~` |
+ /// | Macron | `macron` | `¯` |
+ /// | Breve | `breve` | `˘` |
+ /// | Dot | `dot` | `.` |
+ /// | Diaeresis | `diaer` | `¨` |
+ /// | Circle | `circle` | `∘` |
+ /// | Double acute | `acute.double` | `˝` |
+ /// | Caron | `caron` | `ˇ` |
+ /// | Right arrow | `arrow`, `->` | `→` |
+ /// | Left arrow | `arrow.l`, `<-` | `←` |
+ #[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()
+}
diff --git a/library/src/math/align.rs b/library/src/math/align.rs
index a9005dfd..6cf13a0f 100644
--- a/library/src/math/align.rs
+++ b/library/src/math/align.rs
@@ -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<()> {
diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs
index 31dc75ab..9181ab7c 100644
--- a/library/src/math/attach.rs
+++ b/library/src/math/attach.rs
@@ -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.
+ #[named]
+ #[default]
pub top: Option<Content>,
+
/// The bottom attachment.
+ #[named]
+ #[default]
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())
- }
-}
-
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)
}
}
diff --git a/library/src/math/delimited.rs b/library/src/math/delimited.rs
index 511f1a7b..b0126cad 100644
--- a/library/src/math/delimited.rs
+++ b/library/src/math/delimited.rs
@@ -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(),
))
}
diff --git a/library/src/math/frac.rs b/library/src/math/frac.rs
index cdf533da..ea647fc5 100644
--- a/library/src/math/frac.rs
+++ b/library/src/math/frac.rs
@@ -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)
}
}
diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs
index 978d262b..3e257385 100644
--- a/library/src/math/matrix.rs
+++ b/library/src/math/matrix.rs
@@ -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>);
-
-#[node]
-impl VecNode {
+/// Display: Vector
+/// Category: math
+#[node(LayoutMath)]
+pub struct VecNode {
+ /// The elements of the vector.
+ #[variadic]
+ pub elements: Vec<Content>,
+
/// 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>>);
-
-#[node]
-impl MatNode {
+/// 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>>,
+
/// 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>);
-
-#[node]
-impl CasesNode {
+/// Display: Cases
+/// Category: math
+#[node(LayoutMath)]
+pub struct CasesNode {
+ /// The branches of the case distinction.
+ #[variadic]
+ pub branches: Vec<Content>,
+
/// 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,
diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs
index d73b1769..9da12e4f 100644
--- a/library/src/math/mod.rs
+++ b/library/src/math/mod.rs
@@ -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(());
}
diff --git a/library/src/math/op.rs b/library/src/math/op.rs
index 4eb9c48c..c855cd92 100644
--- a/library/src/math/op.rs
+++ b/library/src/math/op.rs
@@ -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.
+ ///
+ /// Defaults to `{false}`.
+ #[named]
+ #[default(false)]
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())
- }
-}
-
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'));
}
diff --git a/library/src/math/root.rs b/library/src/math/root.rs
index e40f56f0..191acb94 100644
--- a/library/src/math/root.rs
+++ b/library/src/math/root.rs
@@ -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,
diff --git a/library/src/math/row.rs b/library/src/math/row.rs
index 1271e49e..b7720c14 100644
--- a/library/src/math/row.rs
+++ b/library/src/math/row.rs
@@ -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()])
}
diff --git a/library/src/math/spacing.rs b/library/src/math/spacing.rs
index 17267238..e1b9d408 100644
--- a/library/src/math/spacing.rs
+++ b/library/src/math/spacing.rs
@@ -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.
diff --git a/library/src/math/style.rs b/library/src/math/style.rs
index 9856a6b0..99365106 100644
--- a/library/src/math/style.rs
+++ b/library/src/math/style.rs
@@ -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(())
}
diff --git a/library/src/math/underover.rs b/library/src/math/underover.rs
index 4387de20..87f30c0f 100644
--- a/library/src/math/underover.rs
+++ b/library/src/math/underover.rs
@@ -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.
+ #[positional]
+ #[default]
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())
- }
-}
-
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.
+ #[positional]
+ #[default]
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())
- }
-}
-
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.
+ #[positional]
+ #[default]
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())
- }
-}
-
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.
+ #[positional]
+ #[default]
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())
- }
-}
-
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)
}
}
diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs
index 1d349b89..0d03b496 100644
--- a/library/src/meta/document.rs
+++ b/library/src/meta/document.rs
@@ -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()
+}
diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs
index f108cad1..38890885 100644
--- a/library/src/meta/heading.rs
+++ b/library/src/meta/heading.rs
@@ -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);
diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs
index 61e75b8a..63a8ec98 100644
--- a/library/src/meta/link.rs
+++ b/library/src/meta/link.rs
@@ -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() })
+}
diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs
index 5b7cd92b..d3e1ee4d 100644
--- a/library/src/meta/numbering.rs
+++ b/library/src/meta/numbering.rs
@@ -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();
diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs
index d9eea0a9..7bc08bf9 100644
--- a/library/src/meta/outline.rs
+++ b/library/src/meta/outline.rs
@@ -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))
}
diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs
index e64751f7..55051b5e 100644
--- a/library/src/meta/reference.rs
+++ b/library/src/meta/reference.rs
@@ -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())))
}
}
diff --git a/library/src/prelude.rs b/library/src/prelude.rs
index a406e586..49afc9ca 100644
--- a/library/src/prelude.rs
+++ b/library/src/prelude.rs
@@ -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};
diff --git a/library/src/shared/behave.rs b/library/src/shared/behave.rs
index ec8fade9..74c4d151 100644
--- a/library/src/shared/behave.rs
+++ b/library/src/shared/behave.rs
@@ -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.
diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs
index 3e600b8d..83e62ac4 100644
--- a/library/src/shared/ext.rs
+++ b/library/src/shared/ext.rs
@@ -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)
- }
-}
diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs
index 4043141b..18145d28 100644
--- a/library/src/text/deco.rs
+++ b/library/src/text/deco.rs
@@ -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);
-
-#[node]
-impl UnderlineNode {
+/// Display: Underline
+/// Category: text
+#[node(Show)]
+pub struct UnderlineNode {
+ /// The content to underline.
+ #[positional]
+ #[required]
+ pub body: Content,
+
/// 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);
-
-#[node]
-impl OverlineNode {
+/// Display: Overline
+/// Category: text
+#[node(Show)]
+pub struct OverlineNode {
+ /// The content to add a line over.
+ #[positional]
+ #[required]
+ pub body: Content,
+
/// 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);
-
-#[node]
-impl StrikeNode {
+/// Display: Strikethrough
+/// Category: text
+#[node(Show)]
+pub struct StrikeNode {
+ /// The content to strike through.
+ #[positional]
+ #[required]
+ pub body: Content,
+
/// 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 {
diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs
index de7974cd..9caaf68e 100644
--- a/library/src/text/misc.rs
+++ b/library/src/text/misc.rs
@@ -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")?;
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
index bdd2d0c2..cde0163e 100644
--- a/library/src/text/mod.rs
+++ b/library/src/text/mod.rs
@@ -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;
+ #[settable]
+ #[fold]
+ #[skip]
+ #[default(0)]
+ pub delta: Delta,
+
/// Whether the font style should be inverted.
- #[property(skip, fold)]
- pub const EMPH: Toggle = false;
+ #[settable]
+ #[fold]
+ #[skip]
+ #[default(false)]
+ pub emph: Toggle,
+
/// A case transformation that should be applied to the text.
- #[property(skip)]
- pub const CASE: Option<Case> = None;
+ #[settable]
+ #[skip]
+ #[default(None)]
+ pub case: Option<Case>,
+
/// Whether small capital glyphs should be used. ("smcp")
- #[property(skip)]
- pub const SMALLCAPS: bool = false;
+ #[settable]
+ #[skip]
+ #[default(false)]
+ pub smallcaps: bool,
+
/// Decorative lines.
- #[property(skip, fold)]
- pub const DECO: Decoration = vec![];
+ #[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;
diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs
index c0a7e11c..1c602871 100644
--- a/library/src/text/quotes.rs
+++ b/library/src/text/quotes.rs
@@ -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)]
+/// Display: Smart Quote
+/// Category: text
+#[node]
pub struct SmartQuoteNode {
+ /// Whether this should be a double quote.
+ #[named]
+ #[default(true)]
pub double: bool,
-}
-#[node]
-impl SmartQuoteNode {
/// 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.
diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs
index ec11582c..cdaefd06 100644
--- a/library/src/text/raw.rs
+++ b/library/src/text/raw.rs
@@ -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.
+ ///
+ /// ````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,
-}
-#[node]
-impl RawNode {
/// 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)
diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs
index feb9b24b..709cce25 100644
--- a/library/src/text/shaping.rs
+++ b/library/src/text/shaping.rs
@@ -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.
diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs
index d6809591..105953b6 100644
--- a/library/src/text/shift.rs
+++ b/library/src/text/shift.rs
@@ -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);
-
-#[node]
-impl SubNode {
+/// Display: Subscript
+/// Category: text
+#[node(Show)]
+pub struct SubNode {
+ /// The text to display in subscript.
+ #[positional]
+ #[required]
+ pub body: Content,
+
/// 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);
-
-#[node]
-impl SuperNode {
+/// Display: Superscript
+/// Category: text
+#[node(Show)]
+pub struct SuperNode {
+ /// The text to display in superscript.
+ #[positional]
+ #[required]
+ pub body: Content,
+
/// 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,
}
diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs
index 5e3c7f83..fa5b70ad 100644
--- a/library/src/visualize/image.rs
+++ b/library/src/visualize/image.rs
@@ -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>>,
-}
-#[node]
-impl ImageNode {
/// How the image should adjust itself to a given area.
- pub const FIT: ImageFit = ImageFit::Cover;
+ #[settable]
+ #[default(ImageFit::Cover)]
+ pub fit: ImageFit,
+}
+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)
+}
diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs
index 553e06c8..0e0a272f 100644
--- a/library/src/visualize/line.rs
+++ b/library/src/visualize/line.rs
@@ -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.
+ #[named]
+ #[default]
pub delta: Axes<Rel<Length>>,
-}
-#[node]
-impl LineNode {
/// 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));
diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs
index e5259d91..ef81a871 100644
--- a/library/src/visualize/shape.rs
+++ b/library/src/visualize/shape.rs
@@ -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>,
+
+ /// 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>>,
-}
-#[node]
-impl RectNode {
/// 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>,
+
+ /// 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>>,
-}
-#[node]
-impl SquareNode {
/// 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>,
+
+ /// 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>>,
-}
-#[node]
-impl EllipseNode {
/// 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>,
+
+ /// 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>>,
-}
-#[node]
-impl CircleNode {
/// 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),
diff --git a/macros/src/capable.rs b/macros/src/capable.rs
deleted file mode 100644
index dcfdfc82..00000000
--- a/macros/src/capable.rs
+++ /dev/null
@@ -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
- }
- }
- })
-}
diff --git a/macros/src/castable.rs b/macros/src/castable.rs
index c39df90a..c0d0c1ad 100644
--- a/macros/src/castable.rs
+++ b/macros/src/castable.rs
@@ -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))
- }
- }
-}
diff --git a/macros/src/func.rs b/macros/src/func.rs
index f65c135e..01c3ca0e 100644
--- a/macros/src/func.rs
+++ b/macros/src/func.rs
@@ -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)?;
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index cd0f9988..c1a8b2ae 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -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)
+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 `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)
+/// Implement `Cast` and optionally `Type` for a type.
+#[proc_macro]
+pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
+ castable::cast_from_value(stream.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
-/// Implement `Cast` and optionally `Type` for a type.
+/// Implement `From<T> for Value` for a type `T`.
#[proc_macro]
-pub fn castable(stream: BoundaryStream) -> BoundaryStream {
- castable::castable(stream.into())
+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")
-}
diff --git a/macros/src/node.rs b/macros/src/node.rs
index 0d59a402..8a6660ca 100644
--- a/macros/src/node.rs
+++ b/macros/src/node.rs
@@ -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>,
+ attrs: Vec<syn::Attribute>,
+ vis: syn::Visibility,
+ ident: Ident,
+ name: String,
+ capable: Vec<Ident>,
+ set: Option<syn::Block>,
+ fields: Vec<Field>,
}
-/// A style property.
-struct Property {
+struct Field {
attrs: Vec<syn::Attribute>,
vis: syn::Visibility,
- name: Ident,
- value_ty: syn::Type,
- output_ty: syn::Type,
- default: syn::Expr,
- skip: bool,
- referenced: bool,
+ ident: Ident,
+ with_ident: Ident,
+ name: String,
+
+ positional: bool,
+ required: bool,
+ variadic: bool,
+
+ named: bool,
shorthand: Option<Shorthand>,
- resolve: bool,
+
+ 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();
+impl Node {
+ fn inherent(&self) -> impl Iterator<Item = &Field> + Clone {
+ self.fields.iter().filter(|field| !field.settable)
+ }
- // 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"),
- };
+ fn settable(&self) -> impl Iterator<Item = &Field> + Clone {
+ self.fields.iter().filter(|field| field.settable)
+ }
+}
- // 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(),
+/// 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 properties = vec![];
- let mut construct = None;
- let mut set = None;
- let mut field = None;
+ let mut fields = vec![];
+ for field in &named.named {
+ let Some(mut ident) = field.ident.clone() else {
+ bail!(field, "expected named field");
+ };
- // 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"),
+ 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 modules = vec![];
+ let mut items = vec![];
+ let scope = quote::format_ident!("__{}_keys", ident);
+
+ 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 #ident: #scope::#ident::Key<#ty>
+ = #scope::#ident::Key(::std::marker::PhantomData);
+ });
+ }
- let mut skip = false;
- let mut shorthand = None;
- let mut referenced = false;
- let mut resolve = false;
- let mut fold = false;
-
- // Parse the `#[property(..)]` attribute.
- let mut stream = tokens.into_iter().peekable();
- while let Some(token) = stream.next() {
- let ident = match token {
- TokenTree::Ident(ident) => ident,
- TokenTree::Punct(_) => continue,
- _ => bail!(token, "invalid token"),
- };
+ quote! {
+ #(#attrs)*
+ #[::typst::eval::func]
+ #[derive(Debug, Clone, Hash)]
+ #[repr(transparent)]
+ #vis struct #ident(::typst::model::Content);
- 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();
- };
+ impl #ident {
+ #new
+ #(#builders)*
- match ident.to_string().as_str() {
- "skip" => skip = true,
- "shorthand" => {
- shorthand = Some(match arg {
- Some(name) => Shorthand::Named(name),
- None => Shorthand::Positional,
- });
+ /// The node's span.
+ pub fn span(&self) -> Option<::typst::syntax::Span> {
+ self.0.span()
}
- "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
+ impl #ident {
+ #(#accessors)*
+ #(#items)*
}
- } 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,
- })
-}
+ 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)
+ }
-/// 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
+ fn pack(self) -> ::typst::model::Content {
+ self.0
+ }
}
- };
-
- 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);
+ #construct
+ #set
- let name = &property.name;
- let attrs = &property.attrs;
- let vis = &property.vis;
- items.push(parse_quote! {
- #(#attrs)*
- #vis const #name: #scope::#name::#key
- = #scope::#name::Key(::std::marker::PhantomData);
- });
- }
-
- let mut body = node.body.clone();
- body.items = items;
+ impl From<#ident> for ::typst::eval::Value {
+ fn from(value: #ident) -> Self {
+ value.0.into()
+ }
+ }
- Ok(quote! {
- #body
+ #[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)
+ }
-/// 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
+ fn properties() -> ::std::vec::Vec<::typst::eval::ParamInfo> {
+ ::std::vec![#(#infos),*]
}
}
- })
+ }
}
-/// Process a single const item.
-fn create_property_module(node: &Node, property: &Property) -> (syn::Type, syn::ItemMod) {
- let params = &node.params;
- let self_args = &node.self_args;
- let name = &property.name;
- let value_ty = &property.value_ty;
- let output_ty = &property.output_ty;
-
- let key = parse_quote! { Key<#value_ty, #self_args> };
- let phantom_args = self_args.iter().filter(|arg| match arg {
- syn::GenericArgument::Type(syn::Type::Path(path)) => {
- node.params.iter().all(|param| match param {
- syn::GenericParam::Const(c) => !path.path.is_ident(&c.ident),
- _ => true,
- })
- }
- _ => true,
- });
+/// 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 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);
+ 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)
+ }
+ } else if field.resolve {
+ quote! {
+ ::typst::model::Resolve::resolve(
+ values.next().unwrap_or(#default),
+ chain
+ )
+ }
+ } 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
- }
-
- #copy_assertion
- };
-
- // Generate the module code.
- let module = parse_quote! {
- #[allow(non_snake_case)]
- pub mod #name { #scope }
- };
-
- (key, module)
-}
+ impl ::typst::model::Key for Key<#ty> {
+ type Value = #ty;
+ type Output = #output;
-/// Create the property's node method.
-fn create_property_name_const(node: &Node, property: &Property) -> syn::ImplItemConst {
- // The display name, e.g. `TextNode::BOLD`.
- let name = format!("{}::{}", node.self_name, &property.name);
- parse_quote! {
- const NAME: &'static str = #name;
- }
-}
-
-/// Create the property's node method.
-fn create_property_node_func(node: &Node) -> syn::ImplItemMethod {
- let self_ty = &node.self_ty;
- parse_quote! {
- fn node() -> ::typst::model::NodeId {
- ::typst::model::NodeId::of::<#self_ty>()
- }
- }
-}
-
-/// 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;
+ fn id() -> ::typst::model::KeyId {
+ static META: ::typst::model::KeyMeta = ::typst::model::KeyMeta {
+ name: #name,
+ };
+ ::typst::model::KeyId::from_meta(&META)
+ }
- 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,
+ fn node() -> ::typst::model::NodeId {
+ ::typst::model::NodeId::of::<#node_ident>()
}
- }
- } 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,
+
+ fn get(
+ chain: ::typst::model::StyleChain,
+ mut values: impl ::std::iter::Iterator<Item = Self::Value>,
+ ) -> Self::Output {
+ #value
}
}
- } 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
- }
+ // Generate the module code.
+ quote! {
+ pub mod #ident { #scope }
}
}
-/// 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>();
- };
+/// 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
}
- })
+ }
}
diff --git a/macros/src/symbols.rs b/macros/src/symbols.rs
index efa4834d..cdb7f5d7 100644
--- a/macros/src/symbols.rs
+++ b/macros/src/symbols.rs
@@ -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) }
- }
-}
diff --git a/macros/src/util.rs b/macros/src/util.rs
new file mode 100644
index 00000000..c8c56a05
--- /dev/null
+++ b/macros/src/util.rs
@@ -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()
+}
diff --git a/src/doc.rs b/src/doc.rs
index cba3ca99..9ac2f68d 100644
--- a/src/doc.rs
+++ b/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()),
+ })
}
diff --git a/src/eval/cast.rs b/src/eval/cast.rs
index 77521f7f..840ceb05 100644
--- a/src/eval/cast.rs
+++ b/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,88 +26,6 @@ pub trait Cast<V = Value>: Sized {
}
}
-/// Describes a possible value for a cast.
-#[derive(Debug, Clone, Hash)]
-pub enum CastInfo {
- /// Any value is okay.
- Any,
- /// A specific value, plus short documentation for that value.
- Value(Value, &'static str),
- /// Any value of a type.
- Type(&'static str),
- /// Multiple alternatives.
- Union(Vec<Self>),
-}
-
-impl CastInfo {
- /// Produce an error message describing what was expected and what was
- /// found.
- pub fn error(&self, found: &Value) -> EcoString {
- fn accumulate(
- info: &CastInfo,
- found: &Value,
- parts: &mut Vec<EcoString>,
- matching_type: &mut bool,
- ) {
- match info {
- CastInfo::Any => parts.push("anything".into()),
- CastInfo::Value(value, _) => {
- parts.push(value.repr().into());
- if value.type_name() == found.type_name() {
- *matching_type = true;
- }
- }
- CastInfo::Type(ty) => parts.push((*ty).into()),
- CastInfo::Union(options) => {
- for option in options {
- accumulate(option, found, parts, matching_type);
- }
- }
- }
- }
-
- let mut matching_type = false;
- let mut parts = vec![];
- accumulate(self, found, &mut parts, &mut matching_type);
-
- let mut msg = String::from("expected ");
- if parts.is_empty() {
- msg.push_str(" nothing");
- }
-
- crate::diag::comma_list(&mut msg, &parts, "or");
-
- if !matching_type {
- msg.push_str(", found ");
- msg.push_str(found.type_name());
- }
-
- msg.into()
- }
-}
-
-impl Add for CastInfo {
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self {
- Self::Union(match (self, rhs) {
- (Self::Union(mut lhs), Self::Union(rhs)) => {
- lhs.extend(rhs);
- lhs
- }
- (Self::Union(mut lhs), rhs) => {
- lhs.push(rhs);
- lhs
- }
- (lhs, Self::Union(mut rhs)) => {
- rhs.insert(0, lhs);
- rhs
- }
- (lhs, rhs) => vec![lhs, rhs],
- })
- }
-}
-
impl Cast for Value {
fn is(_: &Value) -> bool {
true
@@ -157,43 +69,15 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
}
}
-castable! {
- Dir: "direction",
-}
-
-castable! {
- GenAlign: "alignment",
-}
-
-castable! {
- Regex: "regular expression",
+cast_to_value! {
+ v: u8 => Value::Int(v as i64)
}
-castable! {
- Selector: "selector",
- text: EcoString => Self::text(&text),
- label: Label => Self::Label(label),
- func: Func => func.select(None)?,
- regex: Regex => Self::Regex(regex),
+cast_to_value! {
+ v: u16 => Value::Int(v as i64)
}
-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! {
+cast_from_value! {
u32,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
@@ -204,7 +88,15 @@ castable! {
})?,
}
-castable! {
+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 {
@@ -215,7 +107,11 @@ castable! {
})?,
}
-castable! {
+cast_to_value! {
+ v: usize => Value::Int(v as i64)
+}
+
+cast_from_value! {
NonZeroUsize,
int: i64 => int
.try_into()
@@ -227,12 +123,11 @@ castable! {
})?,
}
-castable! {
- Paint,
- color: Color => Self::Solid(color),
+cast_to_value! {
+ v: NonZeroUsize => Value::Int(v.get() as i64)
}
-castable! {
+cast_from_value! {
char,
string: Str => {
let mut chars = string.chars();
@@ -243,131 +138,30 @@ castable! {
},
}
-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),
+cast_to_value! {
+ v: char => Value::Str(v.into())
}
-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,
+cast_to_value! {
+ v: &str => Value::Str(v.into())
}
-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,
+cast_from_value! {
+ EcoString,
+ v: Str => v.into(),
}
-castable! {
- FontStretch,
- v: Ratio => Self::from_ratio(v.get() as f32),
+cast_to_value! {
+ v: EcoString => Value::Str(v.into())
}
-castable! {
- Lang,
- string: EcoString => Self::from_str(&string)?,
+cast_from_value! {
+ String,
+ v: Str => v.into(),
}
-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")
- }
+cast_to_value! {
+ v: String => Value::Str(v.into())
}
impl<T: Cast> Cast for Option<T> {
@@ -388,126 +182,130 @@ impl<T: Cast> Cast for Option<T> {
}
}
-/// Castable from [`Value::Auto`].
-pub struct AutoValue;
+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 Cast for AutoValue {
+impl<T: Cast> Cast for Vec<T> {
fn is(value: &Value) -> bool {
- matches!(value, Value::Auto)
+ Array::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
- match value {
- Value::Auto => Ok(Self),
- _ => <Self as Cast>::error(value),
- }
+ value.cast::<Array>()?.into_iter().map(Value::cast).collect()
}
fn describe() -> CastInfo {
- CastInfo::Type("auto")
+ <Array as Cast>::describe()
}
}
-impl<T: Cast> Cast for Smart<T> {
- fn is(value: &Value) -> bool {
- matches!(value, Value::Auto) || T::is(value)
+impl<T: Into<Value>> From<Vec<T>> for Value {
+ fn from(v: Vec<T>) -> Self {
+ Value::Array(v.into_iter().map(Into::into).collect())
}
+}
- 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),
+/// Describes a possible value for a cast.
+#[derive(Debug, Clone, Hash)]
+pub enum CastInfo {
+ /// Any value is okay.
+ Any,
+ /// A specific value, plus short documentation for that value.
+ Value(Value, &'static str),
+ /// Any value of a type.
+ Type(&'static str),
+ /// Multiple alternatives.
+ Union(Vec<Self>),
+}
+
+impl CastInfo {
+ /// Produce an error message describing what was expected and what was
+ /// found.
+ pub fn error(&self, found: &Value) -> EcoString {
+ fn accumulate(
+ info: &CastInfo,
+ found: &Value,
+ parts: &mut Vec<EcoString>,
+ matching_type: &mut bool,
+ ) {
+ match info {
+ CastInfo::Any => parts.push("anything".into()),
+ CastInfo::Value(value, _) => {
+ parts.push(value.repr().into());
+ if value.type_name() == found.type_name() {
+ *matching_type = true;
+ }
+ }
+ CastInfo::Type(ty) => parts.push((*ty).into()),
+ CastInfo::Union(options) => {
+ for option in options {
+ accumulate(option, found, parts, matching_type);
+ }
+ }
+ }
}
- }
- fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("auto")
- }
-}
+ let mut matching_type = false;
+ let mut parts = vec![];
+ accumulate(self, found, &mut parts, &mut matching_type);
-impl<T> Cast for Sides<Option<T>>
-where
- T: Cast + Copy,
-{
- fn is(value: &Value) -> bool {
- matches!(value, Value::Dict(_)) || T::is(value)
- }
+ let mut msg = String::from("expected ");
+ if parts.is_empty() {
+ msg.push_str(" nothing");
+ }
- 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)
+ crate::diag::comma_list(&mut msg, &parts, "or");
+
+ if !matching_type {
+ msg.push_str(", found ");
+ msg.push_str(found.type_name());
}
+
+ msg.into()
}
+}
- fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("dictionary")
+impl Add for CastInfo {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self {
+ Self::Union(match (self, rhs) {
+ (Self::Union(mut lhs), Self::Union(rhs)) => {
+ lhs.extend(rhs);
+ lhs
+ }
+ (Self::Union(mut lhs), rhs) => {
+ lhs.push(rhs);
+ lhs
+ }
+ (lhs, Self::Union(mut rhs)) => {
+ rhs.insert(0, lhs);
+ rhs
+ }
+ (lhs, rhs) => vec![lhs, rhs],
+ })
}
}
-impl<T> Cast for Corners<Option<T>>
-where
- T: Cast + Copy,
-{
- fn is(value: &Value) -> bool {
- matches!(value, Value::Dict(_)) || T::is(value)
+/// Castable from nothing.
+pub enum Never {}
+
+impl Cast for Never {
+ fn is(_: &Value) -> bool {
+ false
}
- 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 cast(value: Value) -> StrResult<Self> {
+ <Self as Cast>::error(value)
}
fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("dictionary")
+ CastInfo::Union(vec![])
}
}
diff --git a/src/eval/func.rs b/src/eval/func.rs
index e5280932..8243b4f6 100644
--- a/src/eval/func.rs
+++ b/src/eval/func.rs
@@ -1,3 +1,5 @@
+pub use typst_macros::func;
+
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
diff --git a/src/eval/library.rs b/src/eval/library.rs
index adfcc6e7..75787348 100644
--- a/src/eval/library.rs
+++ b/src/eval/library.rs
@@ -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.
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 2cf6f4d1..8180f11d 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -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::*;
diff --git a/src/eval/str.rs b/src/eval/str.rs
index 63ea5dc8..0d5d71b9 100644
--- a/src/eval/str.rs
+++ b/src/eval/str.rs
@@ -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,
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 5e06da76..9b9bc314 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -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) {
diff --git a/src/font/mod.rs b/src/font/mod.rs
index bedc107d..94ec170e 100644
--- a/src/font/mod.rs
+++ b/src/font/mod.rs
@@ -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",
+ })
+}
diff --git a/src/font/variant.rs b/src/font/variant.rs
index aa9ff141..4eda80ad 100644
--- a/src/font/variant.rs
+++ b/src/font/variant.rs
@@ -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::*;
diff --git a/src/geom/abs.rs b/src/geom/abs.rs
index 4429e46d..34c3d010 100644
--- a/src/geom/abs.rs
+++ b/src/geom/abs.rs
@@ -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 {
diff --git a/src/geom/align.rs b/src/geom/align.rs
index 1e9bde52..b14e6775 100644
--- a/src/geom/align.rs
+++ b/src/geom/align.rs
@@ -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
+ }
+}
diff --git a/src/geom/axes.rs b/src/geom/axes.rs
index 48f8c0e8..92bd0388 100644
--- a/src/geom/axes.rs
+++ b/src/geom/axes.rs
@@ -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,
+ })
+ }
+}
diff --git a/src/geom/corners.rs b/src/geom/corners.rs
index 386acbfb..844d3047 100644
--- a/src/geom/corners.rs
+++ b/src/geom/corners.rs
@@ -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)
+ }
+}
diff --git a/src/geom/dir.rs b/src/geom/dir.rs
index b2fd6e5a..bc4d66e1 100644
--- a/src/geom/dir.rs
+++ b/src/geom/dir.rs
@@ -73,3 +73,7 @@ impl Debug for Dir {
})
}
}
+
+cast_from_value! {
+ Dir: "direction",
+}
diff --git a/src/geom/em.rs b/src/geom/em.rs
index 9f5aff39..2c63c81d 100644
--- a/src/geom/em.rs
+++ b/src/geom/em.rs
@@ -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))
+ }
+ }
+}
diff --git a/src/geom/length.rs b/src/geom/length.rs
index ae615f14..f70ea263 100644
--- a/src/geom/length.rs
+++ b/src/geom/length.rs
@@ -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)
+ }
+}
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index ebe4436c..b7daaa1b 100644
--- a/src/geom/mod.rs
+++ b/src/geom/mod.rs
@@ -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
diff --git a/src/geom/paint.rs b/src/geom/paint.rs
index b4064438..c01b21da 100644
--- a/src/geom/paint.rs
+++ b/src/geom/paint.rs
@@ -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 {
diff --git a/src/geom/rel.rs b/src/geom/rel.rs
index a8e75d1c..7288f380 100644
--- a/src/geom/rel.rs
+++ b/src/geom/rel.rs
@@ -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
+ }
+}
diff --git a/src/geom/shape.rs b/src/geom/shape.rs
new file mode 100644
index 00000000..5658c21f
--- /dev/null
+++ b/src/geom/shape.rs
@@ -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) }
+ }
+}
diff --git a/src/geom/sides.rs b/src/geom/sides.rs
index 40327a42..247d9a98 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -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,
+ })
+ }
+}
diff --git a/src/geom/smart.rs b/src/geom/smart.rs
index e115e99d..c977d651 100644
--- a/src/geom/smart.rs
+++ b/src/geom/smart.rs
@@ -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,
+ }
+ }
+}
diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs
index 86191d33..500a4c10 100644
--- a/src/geom/stroke.rs
+++ b/src/geom/stroke.rs
@@ -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),
+ }
+ }
+}
diff --git a/src/ide/complete.rs b/src/ide/complete.rs
index 06ab53a1..f5eece93 100644
--- a/src/ide/complete.rs
+++ b/src/ide/complete.rs
@@ -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);
diff --git a/src/lib.rs b/src/lib.rs
index d73055d1..7cfed897 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/src/model/content.rs b/src/model/content.rs
index b10a3409..2af4ae72 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -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;
- }
-
- if let Some(styled) = self.to_mut::<StyledNode>() {
- styled.map.apply(styles);
- 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()
}
-
- 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(),
- }
- }
+ fn pack(self) -> Content;
+}
- /// A unique identifier of the node type.
- fn id(&self) -> NodeId;
+/// A unique identifier for a node.
+#[derive(Copy, Clone)]
+pub struct NodeId(&'static NodeMeta);
- /// The node's name.
- fn name(&self) -> &'static str;
+impl NodeId {
+ pub fn of<T: Node>() -> Self {
+ T::id()
+ }
- /// 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;
+ pub fn from_meta(meta: &'static NodeMeta) -> Self {
+ Self(meta)
+ }
- /// 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;
+ /// The name of the identified node.
+ pub fn name(self) -> &'static str {
+ self.0.name
+ }
+}
- /// List the settable properties.
- fn properties() -> Vec<ParamInfo>
- where
- Self: Sized;
+impl Debug for NodeId {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.name())
+ }
+}
- /// Access a field on this node.
- fn field(&self, name: &str) -> Option<Value>;
+impl Hash for NodeId {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ state.write_usize(self.0 as *const _ as usize);
+ }
}
-/// A unique identifier for a node type.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct NodeId(ReadableTypeId);
+impl Eq for NodeId {}
-impl NodeId {
- /// The id of the given node type.
- pub fn of<T: 'static>() -> Self {
- Self(ReadableTypeId::of::<T>())
+impl PartialEq for NodeId {
+ fn eq(&self, other: &Self) -> bool {
+ std::ptr::eq(self.0, other.0)
}
}
-impl Debug for NodeId {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
- }
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct NodeMeta {
+ pub name: &'static str,
+ pub vtable: fn(of: TypeId) -> Option<*const ()>,
}
-/// A capability a node can have.
-///
-/// Should be implemented by trait objects that are accessible through
-/// [`Capable`].
-pub trait Capability: 'static {}
-
-/// 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 ()>;
+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")
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 692d18d5..07329e3f 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -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;
diff --git a/src/model/realize.rs b/src/model/realize.rs
index b33cc0bb..2f38df51 100644
--- a/src/model/realize.rs
+++ b/src/model/realize.rs
@@ -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
diff --git a/src/model/styles.rs b/src/model/styles.rs
index 18507491..cbf4cfb2 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -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
- }
-}
diff --git a/src/model/typeset.rs b/src/model/typeset.rs
index f8b5e012..6361e6ce 100644
--- a/src/model/typeset.rs
+++ b/src/model/typeset.rs
@@ -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));
}
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index ce0ed1aa..54b9fe27 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -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,
diff --git a/tests/ref/text/raw-code.png b/tests/ref/text/raw-code.png
index 25735e9d..b06b4e4e 100644
--- a/tests/ref/text/raw-code.png
+++ b/tests/ref/text/raw-code.png
Binary files differ
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index 0c2b0490..eae0126d 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -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!("> ");
diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ
index 7fa55890..6416043c 100644
--- a/tests/typ/compiler/show-node.typ
+++ b/tests/typ/compiler/show-node.typ
@@ -2,7 +2,7 @@
---
// Override lists.
-#show list: it => "(" + it.items.join(", ") + ")"
+#show list: it => "(" + it.items.map(item => item.body).join(", ") + ")"
- A
- B
diff --git a/tests/typ/layout/terms.typ b/tests/typ/layout/terms.typ
index 6e828094..0be8ddc7 100644
--- a/tests/typ/layout/terms.typ
+++ b/tests/typ/layout/terms.typ
@@ -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
diff --git a/tests/typ/math/frac.typ b/tests/typ/math/frac.typ
index db37e28c..2e915de2 100644
--- a/tests/typ/math/frac.typ
+++ b/tests/typ/math/frac.typ
@@ -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) $
---
diff --git a/tests/typ/text/edge.typ b/tests/typ/text/edge.typ
index 802c0ddb..8c4c4a57 100644
--- a/tests/typ/text/edge.typ
+++ b/tests/typ/text/edge.typ
@@ -17,9 +17,9 @@
#try(1pt + 0.3em, -0.15em)
---
-// Error: 21-23 expected length, "ascender", "cap-height", "x-height", "baseline", or "descender", found array
+// Error: 21-23 expected "ascender", "cap-height", "x-height", "baseline", "descender", or length, found array
#set text(top-edge: ())
---
-// Error: 24-26 expected length, "ascender", "cap-height", "x-height", "baseline", or "descender"
+// Error: 24-26 expected "ascender", "cap-height", "x-height", "baseline", "descender", or length
#set text(bottom-edge: "")