Fully untyped model

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

View File

@ -1,3 +1,5 @@
//! Calculations and processing of numeric values.
use std::cmp::Ordering;
use std::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),

View File

@ -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")?;

View File

@ -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 } =

View File

@ -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")?;

View File

@ -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::*;

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
use typst::model::Style;
use typst::model::{Style, StyledNode};
use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode};
use 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);

View File

@ -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.

View File

@ -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]))
}
}

View File

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

View File

@ -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 {}

View File

@ -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

View File

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

View File

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

View File

@ -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()
}
}

View File

@ -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();

View File

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

View File

@ -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);

View File

@ -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(),
}
}
}

View File

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

View File

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

View File

@ -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(),
}
}

View File

@ -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()
}

View File

@ -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<()> {

View File

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

View File

@ -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(),
))
}

View File

@ -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)
}
}

View File

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

View File

@ -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(());
}

View File

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

View File

@ -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,

View File

@ -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()])
}

View File

@ -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.

View File

@ -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(())
}

View File

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

View File

@ -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()
}

View File

@ -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);

View File

@ -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() })
}

View File

@ -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();

View File

@ -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))
}

View File

@ -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())))
}
}

View File

@ -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};

View File

@ -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.

View File

@ -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)
}
}

View File

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

View File

@ -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")?;

View File

@ -22,7 +22,6 @@ use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontM
use crate::layout::ParNode;
use crate::prelude::*;
/// # Text
/// Customize the look and layout of text in a variety of ways.
///
/// This function is used often, both with set rules and directly. While the set
@ -62,26 +61,50 @@ use crate::prelude::*;
/// - body: `Content` (positional, required)
/// Content in which all text is styled according to the other arguments.
///
/// ## Category
/// text
#[func]
#[capable]
#[derive(Clone, Hash)]
pub struct TextNode(pub EcoString);
/// Display: Text
/// Category: text
#[node(Construct)]
#[set({
if let Some(family) = args.named("family")? {
styles.set(Self::FAMILY, family);
} else {
let mut count = 0;
let mut content = false;
for item in args.items.iter().filter(|item| item.name.is_none()) {
if EcoString::is(&item.value) {
count += 1;
} else if <Content as Cast<Spanned<Value>>>::is(&item.value) {
content = true;
}
}
impl TextNode {
/// Create a new packed text node.
pub fn packed(text: impl Into<EcoString>) -> Content {
Self(text.into()).pack()
// Skip the final string if it's needed as the body.
if constructor && !content && count > 0 {
count -= 1;
}
if count > 0 {
let mut list = Vec::with_capacity(count);
for _ in 0..count {
list.push(args.find()?.unwrap());
}
styles.set(Self::FAMILY, FallbackList(list));
}
}
}
})]
pub struct TextNode {
/// The text.
#[positional]
#[required]
#[skip]
pub text: EcoString,
#[node]
impl TextNode {
/// A prioritized sequence of font families.
#[property(skip, referenced)]
pub const FAMILY: FallbackList =
FallbackList(vec![FontFamily::new("Linux Libertine")]);
#[settable]
#[skip]
#[default(FallbackList(vec![FontFamily::new("Linux Libertine")]))]
pub family: FallbackList,
/// Whether to allow last resort font fallback when the primary font list
/// contains no match. This lets Typst search through all available fonts
@ -100,7 +123,9 @@ impl TextNode {
/// #set text(fallback: false)
/// هذا عربي
/// ```
pub const FALLBACK: bool = true;
#[settable]
#[default(true)]
pub fallback: bool,
/// The desired font style.
///
@ -119,7 +144,9 @@ impl TextNode {
/// #text("Linux Libertine", style: "italic")[Italic]
/// #text("DejaVu Sans", style: "oblique")[Oblique]
/// ```
pub const STYLE: FontStyle = FontStyle::Normal;
#[settable]
#[default(FontStyle::Normal)]
pub style: FontStyle,
/// The desired thickness of the font's glyphs. Accepts an integer between
/// `{100}` and `{900}` or one of the predefined weight names. When the
@ -138,7 +165,9 @@ impl TextNode {
/// #text(weight: 500)[Medium] \
/// #text(weight: "bold")[Bold]
/// ```
pub const WEIGHT: FontWeight = FontWeight::REGULAR;
#[settable]
#[default(FontWeight::REGULAR)]
pub weight: FontWeight,
/// The desired width of the glyphs. Accepts a ratio between `{50%}` and
/// `{200%}`. When the desired weight is not available, Typst selects the
@ -148,7 +177,9 @@ impl TextNode {
/// #text(stretch: 75%)[Condensed] \
/// #text(stretch: 100%)[Normal]
/// ```
pub const STRETCH: FontStretch = FontStretch::NORMAL;
#[settable]
#[default(FontStretch::NORMAL)]
pub stretch: FontStretch,
/// The size of the glyphs. This value forms the basis of the `em` unit:
/// `{1em}` is equivalent to the font size.
@ -160,8 +191,11 @@ impl TextNode {
/// #set text(size: 20pt)
/// very #text(1.5em)[big] text
/// ```
#[property(shorthand, fold)]
pub const SIZE: TextSize = Abs::pt(11.0);
#[settable]
#[shorthand]
#[fold]
#[default(Abs::pt(11.0))]
pub size: TextSize,
/// The glyph fill color.
///
@ -169,8 +203,10 @@ impl TextNode {
/// #set text(fill: red)
/// This text is red.
/// ```
#[property(shorthand)]
pub const FILL: Paint = Color::BLACK.into();
#[shorthand]
#[settable]
#[default(Color::BLACK.into())]
pub fill: Paint,
/// The amount of space that should be added between characters.
///
@ -178,8 +214,10 @@ impl TextNode {
/// #set text(tracking: 1.5pt)
/// Distant text.
/// ```
#[property(resolve)]
pub const TRACKING: Length = Length::zero();
#[settable]
#[resolve]
#[default(Length::zero())]
pub tracking: Length,
/// The amount of space between words.
///
@ -190,8 +228,10 @@ impl TextNode {
/// #set text(spacing: 200%)
/// Text with distant words.
/// ```
#[property(resolve)]
pub const SPACING: Rel<Length> = Rel::one();
#[settable]
#[resolve]
#[default(Rel::one())]
pub spacing: Rel<Length>,
/// An amount to shift the text baseline by.
///
@ -199,8 +239,10 @@ impl TextNode {
/// A #text(baseline: 3pt)[lowered]
/// word.
/// ```
#[property(resolve)]
pub const BASELINE: Length = Length::zero();
#[settable]
#[resolve]
#[default(Length::zero())]
pub baseline: Length,
/// Whether certain glyphs can hang over into the margin in justified text.
/// This can make justification visually more pleasing.
@ -222,7 +264,9 @@ impl TextNode {
/// margin, making the paragraph's
/// edge less clear.
/// ```
pub const OVERHANG: bool = true;
#[settable]
#[default(true)]
pub overhang: bool,
/// The top end of the conceptual frame around the text used for layout and
/// positioning. This affects the size of containers that hold text.
@ -237,7 +281,9 @@ impl TextNode {
/// #set text(top-edge: "cap-height")
/// #rect(fill: aqua)[Typst]
/// ```
pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight);
#[settable]
#[default(TextEdge::Metric(VerticalFontMetric::CapHeight))]
pub top_edge: TextEdge,
/// The bottom end of the conceptual frame around the text used for layout
/// and positioning. This affects the size of containers that hold text.
@ -252,7 +298,9 @@ impl TextNode {
/// #set text(bottom-edge: "descender")
/// #rect(fill: aqua)[Typst]
/// ```
pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
#[settable]
#[default(TextEdge::Metric(VerticalFontMetric::Baseline))]
pub bottom_edge: TextEdge,
/// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
///
@ -271,12 +319,16 @@ impl TextNode {
/// = Einleitung
/// In diesem Dokument, ...
/// ```
pub const LANG: Lang = Lang::ENGLISH;
#[settable]
#[default(Lang::ENGLISH)]
pub lang: Lang,
/// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
///
/// This lets the text processing pipeline make more informed choices.
pub const REGION: Option<Region> = None;
#[settable]
#[default(None)]
pub region: Option<Region>,
/// The dominant direction for text and inline objects. Possible values are:
///
@ -302,8 +354,10 @@ impl TextNode {
/// #set text(dir: rtl)
/// هذا عربي.
/// ```
#[property(resolve)]
pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto);
#[settable]
#[resolve]
#[default(HorizontalDir(Smart::Auto))]
pub dir: HorizontalDir,
/// Whether to hyphenate text to improve line breaking. When `{auto}`, text
/// will be hyphenated if and only if justification is enabled.
@ -322,8 +376,10 @@ impl TextNode {
/// enabling hyphenation can
/// improve justification.
/// ```
#[property(resolve)]
pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto);
#[settable]
#[resolve]
#[default(Hyphenate(Smart::Auto))]
pub hyphenate: Hyphenate,
/// Whether to apply kerning.
///
@ -340,7 +396,9 @@ impl TextNode {
/// #set text(kerning: false)
/// Totally
/// ```
pub const KERNING: bool = true;
#[settable]
#[default(true)]
pub kerning: bool,
/// Whether to apply stylistic alternates.
///
@ -355,14 +413,18 @@ impl TextNode {
/// #set text(alternates: true)
/// 0, a, g, ß
/// ```
pub const ALTERNATES: bool = false;
#[settable]
#[default(false)]
pub alternates: bool,
/// Which stylistic set to apply. Font designers can categorize alternative
/// glyphs forms into stylistic sets. As this value is highly font-specific,
/// you need to consult your font to know which sets are available. When set
/// to an integer between `{1}` and `{20}`, enables the corresponding
/// OpenType font feature from `ss01`, ..., `ss20`.
pub const STYLISTIC_SET: Option<StylisticSet> = None;
#[settable]
#[default(None)]
pub stylistic_set: Option<StylisticSet>,
/// Whether standard ligatures are active.
///
@ -378,15 +440,21 @@ impl TextNode {
/// #set text(ligatures: false)
/// A fine ligature.
/// ```
pub const LIGATURES: bool = true;
#[settable]
#[default(true)]
pub ligatures: bool,
/// Whether ligatures that should be used sparingly are active. Setting this
/// to `{true}` enables the OpenType `dlig` font feature.
pub const DISCRETIONARY_LIGATURES: bool = false;
#[settable]
#[default(false)]
pub discretionary_ligatures: bool,
/// Whether historical ligatures are active. Setting this to `{true}`
/// enables the OpenType `hlig` font feature.
pub const HISTORICAL_LIGATURES: bool = false;
#[settable]
#[default(false)]
pub historical_ligatures: bool,
/// Which kind of numbers / figures to select. When set to `{auto}`, the
/// default numbers for the font are used.
@ -399,7 +467,9 @@ impl TextNode {
/// #set text(number-type: "old-style")
/// Number 9.
/// ```
pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
#[settable]
#[default(Smart::Auto)]
pub number_type: Smart<NumberType>,
/// The width of numbers / figures. When set to `{auto}`, the default
/// numbers for the font are used.
@ -414,7 +484,9 @@ impl TextNode {
/// A 12 B 34. \
/// A 56 B 78.
/// ```
pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
#[settable]
#[default(Smart::Auto)]
pub number_width: Smart<NumberWidth>,
/// Whether to have a slash through the zero glyph. Setting this to `{true}`
/// enables the OpenType `zero` font feature.
@ -422,7 +494,9 @@ impl TextNode {
/// ```example
/// 0, #text(slashed-zero: true)[0]
/// ```
pub const SLASHED_ZERO: bool = false;
#[settable]
#[default(false)]
pub slashed_zero: bool,
/// Whether to turns numbers into fractions. Setting this to `{true}`
/// enables the OpenType `frac` font feature.
@ -431,7 +505,9 @@ impl TextNode {
/// 1/2 \
/// #text(fractions: true)[1/2]
/// ```
pub const FRACTIONS: bool = false;
#[settable]
#[default(false)]
pub fractions: bool,
/// Raw OpenType features to apply.
///
@ -445,74 +521,59 @@ impl TextNode {
/// #set text(features: ("frac",))
/// 1/2
/// ```
#[property(fold)]
pub const FEATURES: FontFeatures = FontFeatures(vec![]);
#[settable]
#[fold]
#[default(FontFeatures(vec![]))]
pub features: FontFeatures,
/// A delta to apply on the font weight.
#[property(skip, fold)]
pub const DELTA: Delta = 0;
/// Whether the font style should be inverted.
#[property(skip, fold)]
pub const EMPH: Toggle = false;
/// A case transformation that should be applied to the text.
#[property(skip)]
pub const CASE: Option<Case> = None;
/// Whether small capital glyphs should be used. ("smcp")
#[property(skip)]
pub const SMALLCAPS: bool = false;
/// Decorative lines.
#[property(skip, fold)]
pub const DECO: Decoration = vec![];
#[settable]
#[fold]
#[skip]
#[default(0)]
pub delta: Delta,
/// Whether the font style should be inverted.
#[settable]
#[fold]
#[skip]
#[default(false)]
pub emph: Toggle,
/// A case transformation that should be applied to the text.
#[settable]
#[skip]
#[default(None)]
pub case: Option<Case>,
/// Whether small capital glyphs should be used. ("smcp")
#[settable]
#[skip]
#[default(false)]
pub smallcaps: bool,
/// Decorative lines.
#[settable]
#[fold]
#[skip]
#[default(vec![])]
pub deco: Decoration,
}
impl TextNode {
/// Create a new packed text node.
pub fn packed(text: impl Into<EcoString>) -> Content {
Self::new(text.into()).pack()
}
}
impl Construct for TextNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
// The text constructor is special: It doesn't create a text node.
// Instead, it leaves the passed argument structurally unchanged, but
// styles all text in it.
args.expect("body")
}
fn set(...) {
if let Some(family) = args.named("family")? {
styles.set(Self::FAMILY, family);
} else {
let mut count = 0;
let mut content = false;
for item in args.items.iter().filter(|item| item.name.is_none()) {
if EcoString::is(&item.value) {
count += 1;
} else if <Content as Cast<Spanned<Value>>>::is(&item.value) {
content = true;
}
}
// Skip the final string if it's needed as the body.
if constructor && !content && count > 0 {
count -= 1;
}
if count > 0 {
let mut list = Vec::with_capacity(count);
for _ in 0..count {
list.push(args.find()?.unwrap());
}
styles.set(Self::FAMILY, FallbackList(list));
}
}
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"text" => Some(Value::Str(self.0.clone().into())),
_ => None,
}
}
}
impl Debug for TextNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Text({:?})", self.0)
}
}
/// A lowercased font family like "arial".
@ -537,21 +598,29 @@ impl Debug for FontFamily {
}
}
castable! {
cast_from_value! {
FontFamily,
string: EcoString => Self::new(&string),
}
cast_to_value! {
v: FontFamily => v.0.into()
}
/// Font family fallback list.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FallbackList(pub Vec<FontFamily>);
castable! {
cast_from_value! {
FallbackList,
family: FontFamily => Self(vec![family]),
values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?),
}
cast_to_value! {
v: FallbackList => v.0.into()
}
/// The size of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TextSize(pub Length);
@ -564,11 +633,15 @@ impl Fold for TextSize {
}
}
castable! {
cast_from_value! {
TextSize,
v: Length => Self(v),
}
cast_to_value! {
v: TextSize => v.0.into()
}
/// Specifies the bottom or top edge of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum TextEdge {
@ -588,34 +661,37 @@ impl TextEdge {
}
}
castable! {
cast_from_value! {
TextEdge,
v: VerticalFontMetric => Self::Metric(v),
v: Length => Self::Length(v),
/// The font's ascender, which typically exceeds the height of all glyphs.
"ascender" => Self::Metric(VerticalFontMetric::Ascender),
/// The approximate height of uppercase letters.
"cap-height" => Self::Metric(VerticalFontMetric::CapHeight),
/// The approximate height of non-ascending lowercase letters.
"x-height" => Self::Metric(VerticalFontMetric::XHeight),
/// The baseline on which the letters rest.
"baseline" => Self::Metric(VerticalFontMetric::Baseline),
/// The font's ascender, which typically exceeds the depth of all glyphs.
"descender" => Self::Metric(VerticalFontMetric::Descender),
}
cast_to_value! {
v: TextEdge => match v {
TextEdge::Metric(metric) => metric.into(),
TextEdge::Length(length) => length.into(),
}
}
/// The direction of text and inline objects in their line.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalDir(pub Smart<Dir>);
castable! {
cast_from_value! {
HorizontalDir,
_: AutoValue => Self(Smart::Auto),
dir: Dir => match dir.axis() {
Axis::X => Self(Smart::Custom(dir)),
Axis::Y => Err("must be horizontal")?,
v: Smart<Dir> => {
if v.map_or(false, |dir| dir.axis() == Axis::Y) {
Err("must be horizontal")?;
}
Self(v)
},
}
cast_to_value! {
v: HorizontalDir => v.0.into()
}
impl Resolve for HorizontalDir {
type Output = Dir;
@ -631,10 +707,13 @@ impl Resolve for HorizontalDir {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub Smart<bool>);
castable! {
cast_from_value! {
Hyphenate,
_: AutoValue => Self(Smart::Auto),
v: bool => Self(Smart::Custom(v)),
v: Smart<bool> => Self(v),
}
cast_to_value! {
v: Hyphenate => v.0.into()
}
impl Resolve for Hyphenate {
@ -664,7 +743,7 @@ impl StylisticSet {
}
}
castable! {
cast_from_value! {
StylisticSet,
v: i64 => match v {
1 ..= 20 => Self::new(v as u8),
@ -672,6 +751,10 @@ castable! {
},
}
cast_to_value! {
v: StylisticSet => v.0.into()
}
/// Which kind of numbers / figures to select.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum NumberType {
@ -681,16 +764,23 @@ pub enum NumberType {
OldStyle,
}
castable! {
cast_from_value! {
NumberType,
/// Numbers that fit well with capital text (the OpenType `lnum`
/// font feature).
"lining" => Self::Lining,
/// Numbers that fit well into a flow of upper- and lowercase text (the
// Numbers that fit well into a flow of upper- and lowercase text (the
/// OpenType `onum` font feature).
"old-style" => Self::OldStyle,
}
cast_to_value! {
v: NumberType => Value::from(match v {
NumberType::Lining => "lining",
NumberType::OldStyle => "old-style",
})
}
/// The width of numbers / figures.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum NumberWidth {
@ -700,7 +790,7 @@ pub enum NumberWidth {
Tabular,
}
castable! {
cast_from_value! {
NumberWidth,
/// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
"proportional" => Self::Proportional,
@ -708,11 +798,18 @@ castable! {
"tabular" => Self::Tabular,
}
cast_to_value! {
v: NumberWidth => Value::from(match v {
NumberWidth::Proportional => "proportional",
NumberWidth::Tabular => "tabular",
})
}
/// OpenType font features settings.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FontFeatures(pub Vec<(Tag, u32)>);
castable! {
cast_from_value! {
FontFeatures,
values: Array => Self(values
.into_iter()
@ -731,6 +828,18 @@ castable! {
.collect::<StrResult<_>>()?),
}
cast_to_value! {
v: FontFeatures => Value::Dict(
v.0.into_iter()
.map(|(tag, num)| {
let bytes = tag.to_bytes();
let key = std::str::from_utf8(&bytes).unwrap_or_default();
(key.into(), num.into())
})
.collect(),
)
}
impl Fold for FontFeatures {
type Output = Self;

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,7 @@
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;
use super::*;
/// 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))
}
}
}

View File

@ -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)?;

View File

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

View File

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

View File

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

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

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

View File

@ -7,14 +7,14 @@ use std::sync::Arc;
use ecow::EcoString;
use 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()),
})
}

View File

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

View File

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

View File

@ -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.

View File

@ -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::*;

View File

@ -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,

View File

@ -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) {

View File

@ -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",
})
}

View File

@ -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::*;

View File

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

View File

@ -115,3 +115,51 @@ impl Debug for GenAlign {
}
}
}
cast_from_value! {
GenAlign: "alignment",
}
cast_from_value! {
Axes<GenAlign>: "2d alignment",
}
cast_from_value! {
Axes<Option<GenAlign>>,
align: GenAlign => {
let mut aligns = Axes::default();
aligns.set(align.axis(), Some(align));
aligns
},
aligns: Axes<GenAlign> => aligns.map(Some),
}
cast_to_value! {
v: Axes<Option<GenAlign>> => match (v.x, v.y) {
(Some(x), Some(y)) => Axes::new(x, y).into(),
(Some(x), None) => x.into(),
(None, Some(y)) => y.into(),
(None, None) => Value::None,
}
}
impl Resolve for GenAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
let dir = item!(dir)(styles);
match self {
Self::Start => dir.start().into(),
Self::End => dir.end().into(),
Self::Specific(align) => align,
}
}
}
impl Fold for GenAlign {
type Output = Self;
fn fold(self, _: Self::Output) -> Self::Output {
self
}
}

View File

@ -2,6 +2,7 @@ use std::any::Any;
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
use 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,
})
}
}

View File

@ -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)
}
}

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ mod ratio;
mod rel;
mod 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

View File

@ -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 {

View File

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

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

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

View File

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

View File

@ -31,6 +31,18 @@ impl<T> Smart<T> {
}
}
/// Map the contained custom value with `f` if it contains a custom value,
/// otherwise returns `default`.
pub fn map_or<F, U>(self, default: U, f: F) -> U
where
F: FnOnce(T) -> U,
{
match self {
Self::Auto => default,
Self::Custom(x) => f(x),
}
}
/// Keeps `self` if it contains a custom value, otherwise returns `other`.
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,
}
}
}

View File

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

View File

@ -338,6 +338,11 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
}
}
Value::Content(content) => {
for (name, value) in content.fields() {
ctx.value_completion(Some(name.clone()), value, false, None);
}
}
Value::Dict(dict) => {
for (name, value) in dict.iter() {
ctx.value_completion(Some(name.clone().into()), value, false, None);

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -1,4 +1,4 @@
use super::{capability, Content, NodeId, Recipe, Selector, StyleChain, Vt};
use super::{Content, NodeId, Recipe, Selector, StyleChain, Vt};
use crate::diag::SourceResult;
/// 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

View File

@ -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
}
}

View File

@ -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));
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -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!("> ");

View File

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

View File

@ -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

View File

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

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