diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs index 9ebce84cb..d4a4bcf6f 100644 --- a/library/src/compute/calc.rs +++ b/library/src/compute/calc.rs @@ -1,3 +1,5 @@ +//! Calculations and processing of numeric values. + use std::cmp::Ordering; use std::ops::Rem; @@ -6,7 +8,7 @@ use typst::eval::{Module, Scope}; use crate::prelude::*; /// A module with computational functions. -pub fn calc() -> Module { +pub fn module() -> Module { let mut scope = Scope::new(); scope.def_func::("abs"); scope.def_func::("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 { Ok(args.expect::("value")?.0) @@ -61,7 +62,7 @@ pub fn abs(args: &mut Args) -> SourceResult { /// 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 { let base = args.expect::("base")?; @@ -103,7 +103,6 @@ pub fn pow(args: &mut Args) -> SourceResult { 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: `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 { let value = args.expect::>("value")?; @@ -127,7 +126,6 @@ pub fn sqrt(args: &mut Args) -> SourceResult { 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 { /// - angle: `AngleLike` (positional, required) /// The angle whose sine to calculate. /// -/// ## Category -/// calculate +/// Display: Sine +/// Category: calculate #[func] pub fn sin(args: &mut Args) -> SourceResult { let arg = args.expect::("angle")?; @@ -156,7 +154,6 @@ pub fn sin(args: &mut Args) -> SourceResult { })) } -/// # 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 { /// - angle: `AngleLike` (positional, required) /// The angle whose cosine to calculate. /// -/// ## Category -/// calculate +/// Display: Cosine +/// Category: calculate #[func] pub fn cos(args: &mut Args) -> SourceResult { let arg = args.expect::("angle")?; @@ -185,7 +182,6 @@ pub fn cos(args: &mut Args) -> SourceResult { })) } -/// # 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 { /// - angle: `AngleLike` (positional, required) /// The angle whose tangent to calculate. /// -/// ## Category -/// calculate +/// Display: Tangent +/// Category: calculate #[func] pub fn tan(args: &mut Args) -> SourceResult { let arg = args.expect::("angle")?; @@ -213,7 +209,6 @@ pub fn tan(args: &mut Args) -> SourceResult { })) } -/// # Arcsine /// Calculate the arcsine of a number. /// /// ## Example @@ -226,8 +221,8 @@ pub fn tan(args: &mut Args) -> SourceResult { /// - 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 { let Spanned { v, span } = args.expect::>("value")?; @@ -238,7 +233,6 @@ pub fn asin(args: &mut Args) -> SourceResult { 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: `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 { let Spanned { v, span } = args.expect::>("value")?; @@ -263,7 +257,6 @@ pub fn acos(args: &mut Args) -> SourceResult { 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: `Num` (positional, required) /// The number whose arctangent to calculate. /// -/// ## Category -/// calculate +/// Display: Arctangent +/// Category: calculate #[func] pub fn atan(args: &mut Args) -> SourceResult { let value = args.expect::("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 { /// - 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 { let arg = args.expect::("angle")?; @@ -311,7 +303,6 @@ pub fn sinh(args: &mut Args) -> SourceResult { })) } -/// # 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 { /// - 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 { let arg = args.expect::("angle")?; @@ -338,7 +329,6 @@ pub fn cosh(args: &mut Args) -> SourceResult { })) } -/// # 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 { /// - 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 { let arg = args.expect::("angle")?; @@ -365,7 +355,6 @@ pub fn tanh(args: &mut Args) -> SourceResult { })) } -/// # 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 { /// - base: `Num` (named) /// The base of the logarithm. /// -/// ## Category -/// calculate +/// Display: Logarithm +/// Category: calculate #[func] pub fn log(args: &mut Args) -> SourceResult { let value = args.expect::("value")?; @@ -390,7 +379,6 @@ pub fn log(args: &mut Args) -> SourceResult { 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: `Num` (positional, required) /// The number to round down. /// -/// ## Category -/// calculate +/// Display: Round down +/// Category: calculate #[func] pub fn floor(args: &mut Args) -> SourceResult { let value = args.expect::("value")?; @@ -417,7 +405,6 @@ pub fn floor(args: &mut Args) -> SourceResult { }) } -/// # 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: `Num` (positional, required) /// The number to round up. /// -/// ## Category -/// calculate +/// Display: Round up +/// Category: calculate #[func] pub fn ceil(args: &mut Args) -> SourceResult { let value = args.expect::("value")?; @@ -444,7 +431,6 @@ pub fn ceil(args: &mut Args) -> SourceResult { }) } -/// # 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 { /// The number to round. /// - digits: `i64` (named) /// -/// ## Category -/// calculate +/// Display: Round +/// Category: calculate #[func] pub fn round(args: &mut Args) -> SourceResult { let value = args.expect::("value")?; @@ -477,7 +463,6 @@ pub fn round(args: &mut Args) -> SourceResult { }) } -/// # Clamp /// Clamp a number between a minimum and maximum value. /// /// ## Example @@ -495,8 +480,8 @@ pub fn round(args: &mut Args) -> SourceResult { /// - max: `Num` (positional, required) /// The inclusive maximum value. /// -/// ## Category -/// calculate +/// Display: Clamp +/// Category: calculate #[func] pub fn clamp(args: &mut Args) -> SourceResult { let value = args.expect::("value")?; @@ -508,7 +493,6 @@ pub fn clamp(args: &mut Args) -> SourceResult { 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 { /// /// - returns: any /// -/// ## Category -/// calculate +/// Display: Minimum +/// Category: calculate #[func] pub fn min(args: &mut Args) -> SourceResult { 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 { /// /// - returns: any /// -/// ## Category -/// calculate +/// Display: Maximum +/// Category: calculate #[func] pub fn max(args: &mut Args) -> SourceResult { minmax(args, Ordering::Greater) @@ -575,7 +558,6 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { Ok(extremum) } -/// # Even /// Determine whether an integer is even. /// /// ## Example @@ -591,14 +573,13 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { /// /// - returns: boolean /// -/// ## Category -/// calculate +/// Display: Even +/// Category: calculate #[func] pub fn even(args: &mut Args) -> SourceResult { Ok(Value::Bool(args.expect::("value")? % 2 == 0)) } -/// # Odd /// Determine whether an integer is odd. /// /// ## Example @@ -615,14 +596,13 @@ pub fn even(args: &mut Args) -> SourceResult { /// /// - returns: boolean /// -/// ## Category -/// calculate +/// Display: Odd +/// Category: calculate #[func] pub fn odd(args: &mut Args) -> SourceResult { Ok(Value::Bool(args.expect::("value")? % 2 != 0)) } -/// # Modulus /// Calculate the modulus of two numbers. /// /// ## Example @@ -640,8 +620,8 @@ pub fn odd(args: &mut Args) -> SourceResult { /// /// - returns: integer or float /// -/// ## Category -/// calculate +/// Display: Modulus +/// Category: calculate #[func] pub fn mod_(args: &mut Args) -> SourceResult { let dividend = args.expect::("dividend")?; @@ -693,7 +673,7 @@ impl Num { } } -castable! { +cast_from_value! { Num, v: i64 => Self::Int(v), v: f64 => Self::Float(v), @@ -706,7 +686,7 @@ enum AngleLike { Angle(Angle), } -castable! { +cast_from_value! { AngleLike, v: i64 => Self::Int(v), v: f64 => Self::Float(v), diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs index 8355e20fd..db4423271 100644 --- a/library/src/compute/construct.rs +++ b/library/src/compute/construct.rs @@ -5,7 +5,6 @@ use typst::eval::Regex; use crate::prelude::*; -/// # Integer /// Convert a value to an integer. /// /// - Booleans are converted to `0` or `1`. @@ -26,8 +25,8 @@ use crate::prelude::*; /// /// - returns: integer /// -/// ## Category -/// construct +/// Display: Integer +/// Category: construct #[func] pub fn int(args: &mut Args) -> SourceResult { Ok(Value::Int(args.expect::("value")?.0)) @@ -36,7 +35,7 @@ pub fn int(args: &mut Args) -> SourceResult { /// 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 { Ok(Value::Float(args.expect::("value")?.0)) @@ -77,7 +75,7 @@ pub fn float(args: &mut Args) -> SourceResult { /// 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 { 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 { /// /// - returns: color /// -/// ## Category -/// construct +/// Display: RGBA +/// Category: construct #[func] pub fn rgb(args: &mut Args) -> SourceResult { Ok(Value::Color(if let Some(string) = args.find::>()? { @@ -175,7 +171,7 @@ pub fn rgb(args: &mut Args) -> SourceResult { /// 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 { let RatioComponent(c) = args.expect("cyan component")?; @@ -231,7 +226,7 @@ pub fn cmyk(args: &mut Args) -> SourceResult { /// 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 { let mut list = EcoVec::new(); @@ -289,7 +283,7 @@ pub fn symbol(args: &mut Args) -> SourceResult { /// 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 { Ok(Value::Str(args.expect::("value")?.0)) @@ -332,7 +325,7 @@ pub fn str(args: &mut Args) -> SourceResult { /// 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 { 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 { /// /// - returns: regex /// -/// ## Category -/// construct +/// Display: Regex +/// Category: construct #[func] pub fn regex(args: &mut Args) -> SourceResult { let Spanned { v, span } = args.expect::>("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 { /// /// - returns: array /// -/// ## Category -/// construct +/// Display: Range +/// Category: construct #[func] pub fn range(args: &mut Args) -> SourceResult { let first = args.expect::("end")?; diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index c604be118..90d72adee 100644 --- a/library/src/compute/data.rs +++ b/library/src/compute/data.rs @@ -4,7 +4,6 @@ use typst::diag::{format_xml_like_error, FileError}; use crate::prelude::*; -/// # Plain text /// Read plain text from a file. /// /// The file will be read and returned as a string. @@ -23,8 +22,8 @@ use crate::prelude::*; /// /// - returns: string /// -/// ## Category -/// data-loading +/// Display: Plain text +/// Category: data-loading #[func] pub fn read(vm: &Vm, args: &mut Args) -> SourceResult { let Spanned { v: path, span } = args.expect::>("path to file")?; @@ -38,7 +37,6 @@ pub fn read(vm: &Vm, args: &mut Args) -> SourceResult { 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 { /// /// - returns: array /// -/// ## Category -/// data-loading +/// Display: CSV +/// Category: data-loading #[func] pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult { let Spanned { v: path, span } = @@ -100,7 +98,7 @@ pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult { /// 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 { 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 { let Spanned { v: path, span } = diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs index 1619fb60f..710ec68ed 100644 --- a/library/src/compute/foundations.rs +++ b/library/src/compute/foundations.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Type /// Determine a value's type. /// /// Returns the name of the value's type. @@ -21,14 +20,13 @@ use crate::prelude::*; /// /// - returns: string /// -/// ## Category -/// foundations +/// Display: Type +/// Category: foundations #[func] pub fn type_(args: &mut Args) -> SourceResult { Ok(args.expect::("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 { /// /// - returns: string /// -/// ## Category -/// foundations +/// Display: Representation +/// Category: foundations #[func] pub fn repr(args: &mut Args) -> SourceResult { Ok(args.expect::("value")?.repr().into()) } -/// # Panic /// Fail with an error. /// /// ## Example @@ -69,8 +66,8 @@ pub fn repr(args: &mut Args) -> SourceResult { /// - payload: `Value` (positional) /// The value (or message) to panic with. /// -/// ## Category -/// foundations +/// Display: Panic +/// Category: foundations #[func] pub fn panic(args: &mut Args) -> SourceResult { match args.eat::()? { @@ -79,7 +76,6 @@ pub fn panic(args: &mut Args) -> SourceResult { } } -/// # 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 { /// - message: `EcoString` (named) /// The error message when the assertion fails. /// -/// ## Category -/// foundations +/// Display: Assert +/// Category: foundations #[func] pub fn assert(args: &mut Args) -> SourceResult { let check = args.expect::("condition")?; @@ -112,7 +108,6 @@ pub fn assert(args: &mut Args) -> SourceResult { 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 { /// /// - returns: any /// -/// ## Category -/// foundations +/// Display: Evaluate +/// Category: foundations #[func] pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult { let Spanned { v: text, span } = args.expect::>("source")?; diff --git a/library/src/compute/mod.rs b/library/src/compute/mod.rs index cf0486db5..3f6a79fc4 100644 --- a/library/src/compute/mod.rs +++ b/library/src/compute/mod.rs @@ -1,11 +1,10 @@ //! Computational functions. -mod calc; +pub mod calc; mod construct; mod data; mod foundations; -pub use self::calc::*; pub use self::construct::*; pub use self::data::*; pub use self::foundations::*; diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs index b84ccfdcb..96c0ae3b9 100644 --- a/library/src/layout/align.rs +++ b/library/src/layout/align.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Align /// Align content horizontally and vertically. /// /// ## Example @@ -13,63 +12,59 @@ use crate::prelude::*; /// A work of art, a visual throne /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to align. -/// -/// - alignment: `Axes>` (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> = 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> = - 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>, +} - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - args.expect("body") - } - - fn set(...) { - let aligns: Axes> = args.find()?.unwrap_or_default(); - styles.set(Self::ALIGNS, aligns); +impl Show for AlignNode { + fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult { + Ok(self.body()) } } diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs index 0353e077d..94c04509a 100644 --- a/library/src/layout/columns.rs +++ b/library/src/layout/columns.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::TextNode; -/// # Columns /// Separate a region into multiple equally sized columns. /// /// The `column` function allows to separate the interior of any container into @@ -31,39 +30,25 @@ use crate::text::TextNode; /// variety of problems. /// ``` /// -/// ## Parameters -/// - count: `usize` (positional, required) -/// The number of columns. -/// -/// - body: `Content` (positional, required) -/// The content that should be layouted into the columns. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Columns +/// Category: layout +#[node(Layout)] pub struct ColumnsNode { - /// How many columns there should be. + /// The number of columns. + #[positional] + #[required] pub count: NonZeroUsize, - /// The child to be layouted into the columns. Most likely, this should be a - /// flow or stack node. + + /// The content that should be layouted into the columns. + #[positional] + #[required] pub body: Content, -} -#[node] -impl ColumnsNode { /// The size of the gutter space between each column. - #[property(resolve)] - pub const GUTTER: Rel = Ratio::new(0.04).into(); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self { - count: args.expect("column count")?, - body: args.expect("body")?, - } - .pack()) - } + #[settable] + #[resolve] + #[default(Ratio::new(0.04).into())] + pub gutter: Rel, } impl Layout for ColumnsNode { @@ -73,14 +58,16 @@ impl Layout for ColumnsNode { styles: StyleChain, regions: Regions, ) -> SourceResult { + 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 { - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { weak }.pack()) - } -} - impl Behave for ColbreakNode { fn behaviour(&self) -> Behaviour { - if self.weak { + if self.weak() { Behaviour::Weak(1) } else { Behaviour::Destructive diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index 8b10f7a69..67504ca3f 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -2,7 +2,6 @@ use super::VNode; use crate::layout::Spacing; use crate::prelude::*; -/// # Box /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot @@ -20,69 +19,75 @@ use crate::prelude::*; /// for more information. /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// The contents of the box. -/// -/// - width: `Sizing` (named) -/// The width of the box. -/// -/// Boxes can have [fractional]($type/fraction) widths, as the example -/// below demonstrates. -/// -/// _Note:_ Currently, only boxes and only their widths might be fractionally -/// sized within paragraphs. Support for fractionally sized images, shapes, -/// and more might be added in the future. -/// -/// ```example -/// Line in #box(width: 1fr, line(length: 100%)) between. -/// ``` -/// -/// - height: `Rel` (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>, -} -#[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>, + /// 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 = Rel::zero(); + #[settable] + #[resolve] + #[default] + pub baseline: Rel, /// The box's background color. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option = None; + #[settable] + #[default] + pub fill: Option, /// The box's border color. See the /// [rectangle's documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Sides>> = Sides::splat(None); + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Sides>>, /// 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>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners>>, /// 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>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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>>, } impl Layout for BoxNode { @@ -116,14 +117,14 @@ impl Layout for BoxNode { styles: StyleChain, regions: Regions, ) -> SourceResult { - 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>` (named) -/// The block's width. -/// -/// ```example -/// #set align(center) -/// #block( -/// width: 60%, -/// inset: 8pt, -/// fill: silver, -/// lorem(10), -/// ) -/// ``` -/// -/// - height: `Smart>` (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>, - /// The box's height. - pub height: Smart>, -} -#[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>, + + /// 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>, + /// 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 = None; + #[settable] + #[default] + pub fill: Option, /// The block's border color. See the /// [rectangle's documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Sides>> = Sides::splat(None); + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Sides>>, /// 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>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners>>, /// 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>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides>>, - /// 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 { - 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 { // 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 for Sizing { - fn from(spacing: Spacing) -> Self { - match spacing { +impl> From 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 => Self::Auto, + v: Rel => Self::Rel(v), + v: Fr => Self::Fr(v), +} + +cast_to_value! { + v: Sizing => match v { + Sizing::Auto => Value::Auto, + Sizing::Rel(rel) => Value::Relative(rel), + Sizing::Fr(fr) => Value::Fraction(fr), + } +} diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 53fc33276..990c0fb96 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -1,11 +1,12 @@ use std::str::FromStr; -use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, ParNode, Sizing, Spacing}; use crate::meta::{Numbering, NumberingPattern}; use crate::prelude::*; use crate::text::TextNode; -/// # Numbered List +use super::GridLayouter; + /// A numbered list. /// /// Displays a sequence of items vertically and numbers them consecutively. @@ -89,20 +90,19 @@ use crate::text::TextNode; /// items. /// ``` /// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Numbered List +/// Category: layout +#[node(Construct, Layout)] pub struct EnumNode { - /// If true, the items are separated by leading instead of list spacing. - pub tight: bool, - /// The individual numbered items. - pub items: StyleVec<(Option, Content)>, -} + /// The numbered list's items. + #[variadic] + pub items: Vec, + + /// 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 = Smart::Auto; + #[settable] + #[default] + pub spacing: Smart, /// 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 { - 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::()?; + if let Some(number) = args.named::("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 { - 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, + + /// 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::().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; diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index ee845f06b..ea31752b9 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -1,4 +1,4 @@ -use typst::model::Style; +use typst::model::{Style, StyledNode}; use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode}; use crate::prelude::*; @@ -8,12 +8,12 @@ use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode} /// /// This node is responsible for layouting both the top-level content flow and /// the contents of boxes. -#[capable(Layout)] -#[derive(Hash)] -pub struct FlowNode(pub StyleVec); - -#[node] -impl FlowNode {} +#[node(Layout)] +pub struct FlowNode { + /// The children that will be arranges into a flow. + #[variadic] + pub children: Vec, +} impl Layout for FlowNode { fn layout( @@ -24,9 +24,17 @@ impl Layout for FlowNode { ) -> SourceResult { let mut layouter = FlowLayouter::new(regions); - for (child, map) in self.0.iter() { - let styles = styles.chain(&map); - if let Some(&node) = child.to::() { + for mut child in self.children() { + let map; + let outer = styles; + let mut styles = outer; + if let Some(node) = child.to::() { + map = node.map(); + styles = outer.chain(&map); + child = node.sub(); + } + + if let Some(node) = child.to::() { layouter.layout_spacing(node, styles); } else if let Some(node) = child.to::() { 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::() { - layouter.layout_multiple(vt, child, styles)?; + layouter.layout_multiple(vt, &child, styles)?; } else if child.is::() { 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::().unwrap(); @@ -204,7 +205,7 @@ impl<'a> FlowLayouter<'a> { } // How to align the block. - let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); + let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles); // Layout the block itself. let sticky = styles.get(BlockNode::STICKY); diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index d0df8794b..d3758fd61 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -3,7 +3,6 @@ use crate::text::TextNode; use super::Sizing; -/// # Grid /// Arrange content in a grid. /// /// The grid element allows you to arrange content in a grid. You can define the @@ -61,64 +60,50 @@ use super::Sizing; /// ``` /// /// ## Parameters -/// - cells: `Content` (positional, variadic) The contents of the table cells. -/// -/// The cells are populated in row-major order. -/// -/// - rows: `TrackSizings` (named) Defines the row sizes. -/// -/// If there are more cells than fit the defined rows, the last row is -/// repeated until there are no more cells. -/// -/// - columns: `TrackSizings` (named) Defines the column sizes. -/// -/// Either specify a track size array or provide an integer to create a grid -/// with that many `{auto}`-sized columns. Note that opposed to rows and -/// gutters, providing a single track size will only ever create a single -/// column. -/// -/// - gutter: `TrackSizings` (named) Defines the gaps between rows & columns. +/// - gutter: `TrackSizings` (named) +/// Defines the gaps between rows & columns. /// /// If there are more gutters than defined sizes, the last gutter is repeated. /// -/// - column-gutter: `TrackSizings` (named) Defines the gaps between columns. -/// Takes precedence over `gutter`. -/// -/// - row-gutter: `TrackSizings` (named) Defines the gaps between rows. Takes -/// precedence over `gutter`. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Grid +/// Category: layout +#[node(Layout)] pub struct GridNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes>, - /// 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, -} -#[node] -impl GridNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { // 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); -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::>()?), } -castable! { - Sizing, - _: AutoValue => Self::Auto, - v: Rel => Self::Rel(v), - v: Fr => Self::Fr(v), +cast_to_value! { + v: TrackSizings => v.0.into() } /// Performs grid layout. diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs index 019dd2a6c..5ba7dea40 100644 --- a/library/src/layout/hide.rs +++ b/library/src/layout/hide.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Hide /// Hide content without affecting layout. /// /// The `hide` function allows you to hide content while the layout still 'sees' @@ -14,26 +13,18 @@ use crate::prelude::*; /// #hide[Hello] Joe /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to hide. -/// -/// ## Category -/// layout -#[func] -#[capable(Show)] -#[derive(Debug, Hash)] -pub struct HideNode(pub Content); - -#[node] -impl HideNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - Ok(self.0.clone().styled(Meta::DATA, vec![Meta::Hidden])) + Ok(self.body().styled(MetaNode::DATA, vec![Meta::Hidden])) } } diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs index e83b91ab1..0ca5ccf66 100644 --- a/library/src/layout/list.rs +++ b/library/src/layout/list.rs @@ -1,8 +1,9 @@ -use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, ParNode, Sizing, Spacing}; use crate::prelude::*; use crate::text::TextNode; -/// # Bullet List +use super::GridLayouter; + /// A bullet list. /// /// Displays a sequence of items vertically, with each item introduced by a @@ -33,48 +34,40 @@ use crate::text::TextNode; /// paragraphs and other block-level content. All content that is indented /// more than an item's hyphen becomes part of that item. /// -/// ## Parameters -/// - items: `Content` (positional, variadic) -/// The list's children. -/// -/// When using the list syntax, adjacent items are automatically collected -/// into lists, even through constructs like for loops. -/// -/// ```example -/// #for letter in "ABC" [ -/// - Letter #letter -/// ] -/// ``` -/// -/// - tight: `bool` (named) -/// If this is `{false}`, the items are spaced apart with [list -/// spacing]($func/list.spacing). If it is `{true}`, they use normal -/// [leading]($func/par.leading) instead. This makes the list more compact, -/// which can look better if the items are short. -/// -/// ```example -/// - If a list has a lot of text, and -/// maybe other inline content, it -/// should not be tight anymore. -/// -/// - To make a list wide, simply insert -/// a blank line between the items. -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Bullet List +/// Category: layout +#[node(Layout)] pub struct ListNode { - /// If true, the items are separated by leading instead of list spacing. - pub tight: bool, - /// The individual bulleted or numbered items. - pub items: StyleVec, -} + /// 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, + + /// 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 = Smart::Auto; + #[settable] + #[default] + pub spacing: Smart, /// The nesting depth. - #[property(skip, fold)] - const DEPTH: Depth = 0; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self { - tight: args.named("tight")?.unwrap_or(true), - items: args.all()?.into_iter().collect(), - } - .pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { 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::().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + /// A list's marker. #[derive(Debug, Clone, Hash)] -pub enum Marker { +pub enum ListMarker { Content(Vec), Func(Func), } -impl Marker { +impl ListMarker { /// Resolve the marker for the given depth. fn resolve(&self, world: Tracked, depth: usize) -> SourceResult { Ok(match self { @@ -203,8 +204,8 @@ impl Marker { } } -castable! { - Marker, +cast_from_value! { + ListMarker, v: Content => Self::Content(vec![v]), array: Array => { if array.len() == 0 { @@ -215,14 +216,28 @@ castable! { v: Func => Self::Func(v), } -#[derive(Debug, Clone, Hash)] +cast_to_value! { + v: ListMarker => match v { + ListMarker::Content(vec) => vec.into(), + ListMarker::Func(func) => func.into(), + } +} + struct Depth; +cast_from_value! { + Depth, + _: Value => Self, +} + +cast_to_value! { + _: Depth => Value::None +} + impl Fold for Depth { type Output = usize; - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer += 1; - outer + fn fold(self, outer: Self::Output) -> Self::Output { + outer + 1 } } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index 9ee77a61f..afdfd7958 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -48,8 +48,8 @@ use std::mem; use typed_arena::Arena; use typst::diag::SourceResult; use typst::model::{ - applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain, - StyleVecBuilder, StyledNode, + applicable, realize, Content, Node, SequenceNode, Style, StyleChain, StyleVecBuilder, + StyledNode, }; use crate::math::{FormulaNode, LayoutMath}; @@ -60,7 +60,6 @@ use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode}; use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode}; /// Root-level layout. -#[capability] pub trait LayoutRoot { /// Layout into one frame per page. fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult; @@ -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>, /// An arena where intermediate content resulting from show rules is stored. content: Arena, + maps: Arena, } 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::() && !content.is::() { - 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::() { - 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::() { - 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::() - .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::() { - self.keep_next = !pagebreak.weak; + self.keep_next = !pagebreak.weak(); return true; } - if let Some(page) = content.to::() { - self.pages.push(page.clone(), styles); + if content.is::() { + self.pages.push(content.clone(), styles); self.keep_next = false; return true; } @@ -441,11 +440,11 @@ impl<'a> FlowBuilder<'a> { if content.has::() || content.is::() { let is_tight_list = if let Some(node) = content.to::() { - node.tight + node.tight() } else if let Some(node) = content.to::() { - node.tight + node.tight() } else if let Some(node) = content.to::() { - 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::() || content.is::() || content.is::() - || content.to::().map_or(false, |node| !node.block) + || content.to::().map_or(false, |node| !node.block()) || content.is::() { 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::() { - 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::()); - return true; - } + if (content.is::() + || content.is::() + || content.is::()) + && 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::()); + 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::() { + ListNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::().unwrap(); + ListItem::new(item.body().styled_with_map(map.clone())) + }) + .collect::>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::() { + EnumNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::().unwrap(); + EnumItem::new(item.body().styled_with_map(map.clone())) + .with_number(item.number()) + }) + .collect::>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::() { + TermsNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::().unwrap(); + TermItem::new( + item.term().styled_with_map(map.clone()), + item.description().styled_with_map(map.clone()), + ) + }) + .collect::>(), + ) + .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, Content), - /// An item of a term list. - Term(TermItem), -} - -#[node] -impl ListItem {} diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs index 4fc2ff298..05aafc762 100644 --- a/library/src/layout/pad.rs +++ b/library/src/layout/pad.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Padding /// Add spacing around content. /// /// The `pad` function adds spacing around content. The spacing can be specified @@ -17,21 +16,6 @@ use crate::prelude::*; /// ``` /// /// ## Parameters -/// - body: `Content` (positional, required) -/// The content to pad at the sides. -/// -/// - left: `Rel` (named) -/// The padding at the left side. -/// -/// - right: `Rel` (named) -/// The padding at the right side. -/// -/// - top: `Rel` (named) -/// The padding at the top side. -/// -/// - bottom: `Rel` (named) -/// The padding at the bottom side. -/// /// - x: `Rel` (named) /// The horizontal padding. Both `left` and `right` take precedence over this. /// @@ -41,20 +25,37 @@ use crate::prelude::*; /// - rest: `Rel` (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>, - /// 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, + + /// The padding at the right side. + #[named] + #[default] + pub right: Rel, + + /// The padding at the top side. + #[named] + #[default] + pub top: Rel, + + /// The padding at the bottom side. + #[named] + #[default] + pub bottom: Rel, } -#[node] -impl PadNode { +impl Construct for PadNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { 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::("body")?; - let padding = Sides::new(left, top, right, bottom); - Ok(Self { padding, body }.pack()) + Ok(Self::new(body) + .with_left(left) + .with_top(top) + .with_bottom(bottom) + .with_right(right) + .pack()) } } @@ -79,9 +84,10 @@ impl Layout for PadNode { let mut backlog = vec![]; // Layout child into padded regions. - let padding = self.padding.resolve(styles); + let sides = Sides::new(self.left(), self.top(), self.right(), self.bottom()); + let padding = sides.resolve(styles); let pod = regions.map(&mut backlog, |size| shrink(size, padding)); - let mut fragment = self.body.layout(vt, styles, pod)?; + let mut fragment = self.body().layout(vt, styles, pod)?; for frame in &mut fragment { // Apply the padding inversely such that the grown size padded diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 022619d7e..5d1d530d8 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use super::ColumnsNode; use crate::prelude::*; -/// # Page /// Layouts its child onto one or multiple pages. /// /// Although this function is primarily used in set rules to affect page @@ -14,13 +13,6 @@ use crate::prelude::*; /// the pages will grow to fit their content on the respective axis. /// /// ## Parameters -/// - body: `Content` (positional, required) -/// The contents of the page(s). -/// -/// Multiple pages will be created if the content does not fit on a single -/// page. A new page with the page properties prior to the function invocation -/// will be created after the body has been typeset. -/// /// - paper: `Paper` (positional, settable) /// A standard paper size to set width and height. When this is not specified, /// Typst defaults to `{"a4"}` paper. @@ -33,15 +25,25 @@ use crate::prelude::*; /// There you go, US friends! /// ``` /// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Clone, Hash)] -pub struct PageNode(pub Content); - +/// Display: Page +/// Category: layout #[node] -impl PageNode { +#[set({ + if let Some(paper) = args.named_or_find::("paper")? { + 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 = Smart::Custom(Paper::A4.width().into()); + #[settable] + #[resolve] + #[default(Smart::Custom(Paper::A4.width().into()))] + pub width: Smart, /// 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 = Smart::Custom(Paper::A4.height().into()); + #[settable] + #[resolve] + #[default(Smart::Custom(Paper::A4.height().into()))] + pub height: Smart, /// 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>>> = Sides::splat(Smart::Auto); + #[settable] + #[fold] + #[default] + pub margin: Sides>>>, /// 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 = None; + #[settable] + #[default] + pub fill: Option, /// The page's header. /// @@ -166,8 +180,9 @@ impl PageNode { /// /// #lorem(18) /// ``` - #[property(referenced)] - pub const HEADER: Option = None; + #[settable] + #[default] + pub header: Option, /// The page's footer. /// @@ -190,8 +205,9 @@ impl PageNode { /// /// #lorem(18) /// ``` - #[property(referenced)] - pub const FOOTER: Option = None; + #[settable] + #[default] + pub footer: Option, /// 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 = None; + #[settable] + #[default] + pub background: Option, /// 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 = None; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } - - fn set(...) { - if let Some(paper) = args.named_or_find::("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 { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } + #[settable] + #[default] + pub foreground: Option, } 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 { - 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. diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 64a6c5131..1b554d624 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -2,7 +2,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use xi_unicode::LineBreakIterator; -use typst::model::Key; +use typst::model::{Key, StyledNode}; use super::{BoxNode, HNode, Sizing, Spacing}; use crate::layout::AlignNode; @@ -12,7 +12,6 @@ use crate::text::{ shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode, }; -/// # Paragraph /// Arrange text, spacing and inline-level nodes into a paragraph. /// /// Although this function is primarily used in set rules to affect paragraph @@ -40,15 +39,14 @@ use crate::text::{ /// - body: `Content` (positional, required) /// The contents of the paragraph. /// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Hash)] -pub struct ParNode(pub StyleVec); +/// Display: Paragraph +/// Category: layout +#[node(Construct)] +pub struct ParNode { + /// The paragraph's children. + #[variadic] + pub children: Vec, -#[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 = Smart::Auto; + #[settable] + #[default] + pub linebreaks: Smart, +} +impl Construct for ParNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { // 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 { 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 { - 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::().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::() { + child = Box::leak(Box::new(node.sub())); + styles = outer.chain(Box::leak(Box::new(node.map()))); + } + let segment = if child.is::() { full.push(' '); Segment::Text(1) } else if let Some(node) = child.to::() { 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::() { + } else if let Some(node) = child.to::() { full.push(SPACING_REPLACE); - Segment::Spacing(node.amount) + Segment::Spacing(node.amount()) } else if let Some(node) = child.to::() { - 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::() { @@ -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::() { - node.0.chars().next() + node.text().chars().next() } else if child.is::() { Some('"') } else if child.is::() || child.is::() { @@ -556,23 +560,25 @@ fn collect<'a>( } }); - full.push_str(quoter.quote("es, node.double, peeked)); + full.push_str(quoter.quote("es, node.double(), peeked)); } else { - full.push(if node.double { '"' } else { '\'' }); + full.push(if node.double() { '"' } else { '\'' }); } Segment::Text(full.len() - prev) } else if let Some(node) = child.to::() { full.push(NODE_REPLACE); Segment::Formula(node) } else if let Some(node) = child.to::() { - 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, + children: &[Content], key: K, -) -> Option> { +) -> Option { children - .styles() - .all(|map| !map.contains(key)) + .iter() + .filter_map(|child| child.to::()) + .all(|node| !node.map().contains(key)) .then(|| styles.get(key)) } diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs index 05de369b7..b4aaf73df 100644 --- a/library/src/layout/place.rs +++ b/library/src/layout/place.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Place /// Place content at an absolute position. /// /// Placed content will not affect the position of other content. Place is @@ -22,48 +21,41 @@ use crate::prelude::*; /// ) /// ``` /// -/// ## Parameters -/// - alignment: `Axes>` (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` (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` (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>, -#[node] -impl PlaceNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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::("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, + + /// The vertical displacement of the placed content. + #[named] + #[default] + pub dy: Rel, } impl Layout for PlaceNode { @@ -83,7 +75,12 @@ impl Layout for PlaceNode { Regions::one(regions.base(), expand) }; - let mut frame = self.0.layout(vt, styles, pod)?.into_frame(); + let child = self + .body() + .moved(Axes::new(self.dx(), self.dy())) + .aligned(self.alignment()); + + let mut frame = child.layout(vt, styles, pod)?.into_frame(); // If expansion is off, zero all sizes so that we don't take up any // space in our parent. Otherwise, respect the expand settings. @@ -99,7 +96,7 @@ impl PlaceNode { /// origin. Instead of relative to the parent's current flow/cursor /// position. pub fn out_of_flow(&self) -> bool { - self.1 + self.alignment().y.is_some() } } diff --git a/library/src/layout/repeat.rs b/library/src/layout/repeat.rs index ec582c280..67dca2857 100644 --- a/library/src/layout/repeat.rs +++ b/library/src/layout/repeat.rs @@ -2,7 +2,6 @@ use crate::prelude::*; use super::AlignNode; -/// # Repeat /// Repeats content to the available space. /// /// This can be useful when implementing a custom index, reference, or outline. @@ -22,22 +21,14 @@ use super::AlignNode; /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to repeat. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] -pub struct RepeatNode(pub Content); - -#[node] -impl RepeatNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = self.0.layout(vt, styles, pod)?.into_frame(); - let align = styles.get(AlignNode::ALIGNS).x.resolve(styles); + let piece = self.body().layout(vt, styles, pod)?.into_frame(); + let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles); let fill = regions.size.x; let width = piece.width(); diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs index a94e48da3..94517ad58 100644 --- a/library/src/layout/spacing.rs +++ b/library/src/layout/spacing.rs @@ -2,7 +2,6 @@ use std::cmp::Ordering; use crate::prelude::*; -/// # Spacing (H) /// Insert horizontal spacing into a paragraph. /// /// The spacing can be absolute, relative, or fractional. In the last case, the @@ -20,64 +19,39 @@ use crate::prelude::*; /// In [mathematical formulas]($category/math), you can additionally use these /// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`. /// -/// ## Parameters -/// - amount: `Spacing` (positional, required) -/// How much spacing to insert. -/// -/// - weak: `bool` (named) -/// If true, the spacing collapses at the start or end of a paragraph. -/// Moreover, from multiple adjacent weak spacings all but the largest one -/// collapse. -/// -/// ```example -/// #h(1cm, weak: true) -/// We identified a group of -/// _weak_ specimens that fail to -/// manifest in most cases. However, -/// when #h(8pt, weak: true) -/// supported -/// #h(8pt, weak: true) on both -/// sides, they do show up. -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable(Behave)] -#[derive(Debug, Copy, Clone, Hash)] +/// Display: Spacing (H) +/// Category: layout +#[node(Behave)] pub struct HNode { - /// The amount of horizontal spacing. + /// How much spacing to insert. + #[positional] + #[required] pub amount: Spacing, - /// Whether the node is weak, see also [`Behaviour`]. + + /// 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 { - 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) -> Self { - Self { amount: amount.into(), weak: false } - } - - /// User-created weak spacing. - pub fn weak(amount: impl Into) -> 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::() 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 { 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::() else { return false }; - self.amount > prev.amount + self.amount() > prev.amount() } } +cast_from_value! { + VNode, + v: Content => v.to::().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 for Spacing { @@ -244,6 +210,12 @@ impl From for Spacing { } } +impl From for Spacing { + fn from(fr: Fr) -> Self { + Self::Fr(fr) + } +} + impl PartialOrd for Spacing { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { @@ -254,8 +226,23 @@ impl PartialOrd for Spacing { } } -castable! { +cast_from_value! { Spacing, v: Rel => Self::Rel(v), v: Fr => Self::Fr(v), } + +cast_to_value! { + v: Spacing => match v { + Spacing::Rel(rel) => { + if rel.rel.is_zero() { + Value::Length(rel.abs) + } else if rel.abs.is_zero() { + Value::Ratio(rel.rel) + } else { + Value::Relative(rel) + } + } + Spacing::Fr(fr) => Value::Fraction(fr), + } +} diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index 864b7b423..430af715a 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -3,7 +3,6 @@ use typst::model::StyledNode; use super::{AlignNode, Spacing}; use crate::prelude::*; -/// # Stack /// Arrange content and spacing horizontally or vertically. /// /// The stack places a list of items along an axis, with optional spacing @@ -19,45 +18,28 @@ use crate::prelude::*; /// ) /// ``` /// -/// ## Parameters -/// - items: `StackChild` (positional, variadic) -/// The items to stack along an axis. -/// -/// - dir: `Dir` (named) -/// The direction along which the items are stacked. Possible values are: -/// -/// - `{ltr}`: Left to right. -/// - `{rtl}`: Right to left. -/// - `{ttb}`: Top to bottom. -/// - `{btt}`: Bottom to top. -/// -/// - spacing: `Spacing` (named) -/// Spacing to insert between items where no explicit spacing was provided. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Stack +/// Category: layout +#[node(Layout)] pub struct StackNode { - /// The stacking direction. - pub dir: Dir, - /// The spacing between non-spacing children. - pub spacing: Option, - /// The children to be stacked. + /// The childfren to stack along the axis. + #[variadic] pub children: Vec, -} -#[node] -impl StackNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, } impl Layout for StackNode { @@ -67,15 +49,16 @@ impl Layout for StackNode { styles: StyleChain, regions: Regions, ) -> SourceResult { - 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::() { - styles.chain(&styled.map).get(AlignNode::ALIGNS) + styles.chain(&styled.map()).get(AlignNode::ALIGNMENT) } else { - styles.get(AlignNode::ALIGNS) + styles.get(AlignNode::ALIGNMENT) }; let aligns = aligns.resolve(styles); diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index 3084c3d41..33ce9088c 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -1,7 +1,6 @@ -use crate::layout::{AlignNode, GridLayouter, Sizing, TrackSizings}; +use crate::layout::{AlignNode, GridLayouter, TrackSizings}; use crate::prelude::*; -/// # Table /// A table of items. /// /// Tables are used to arrange content in cells. Cells can contain arbitrary @@ -31,47 +30,46 @@ use crate::prelude::*; /// ``` /// /// ## Parameters -/// - cells: `Content` (positional, variadic) -/// The contents of the table cells. -/// -/// - rows: `TrackSizings` (named) -/// Defines the row sizes. -/// See the [grid documentation]($func/grid) for more information on track -/// sizing. -/// -/// - columns: `TrackSizings` (named) -/// Defines the column sizes. -/// See the [grid documentation]($func/grid) for more information on track -/// sizing. -/// /// - gutter: `TrackSizings` (named) /// Defines the gaps between rows & columns. /// See the [grid documentation]($func/grid) for more information on gutters. /// -/// - column-gutter: `TrackSizings` (named) -/// Defines the gaps between columns. Takes precedence over `gutter`. -/// See the [grid documentation]($func/grid) for more information on gutters. -/// -/// - row-gutter: `TrackSizings` (named) -/// Defines the gaps between rows. Takes precedence over `gutter`. -/// See the [grid documentation]($func/grid) for more information on gutters. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Table +/// Category: layout +#[node(Layout)] pub struct TableNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes>, - /// The content to be arranged in the table. + /// The contents of the table cells. + #[variadic] pub cells: Vec, -} -#[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> = Celled::Value(None); + #[settable] + #[default] + pub fill: Celled>, /// 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>>> = Celled::Value(Smart::Auto); + #[settable] + #[default] + pub align: Celled>>>, /// 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 = Some(PartialStroke::default()); + #[settable] + #[resolve] + #[fold] + #[default(Some(PartialStroke::default()))] + pub stroke: Option, /// How much to pad the cells's content. /// /// The default value is `{5pt}`. - pub const INSET: Rel = Abs::pt(5.0).into(); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - 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, } 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 Celled { } } +impl Default for Celled { + fn default() -> Self { + Self::Value(T::default()) + } +} + impl Cast for Celled { fn is(value: &Value) -> bool { matches!(value, Value::Func(_)) || T::is(value) @@ -286,3 +268,12 @@ impl Cast for Celled { T::describe() + CastInfo::Type("function") } } + +impl> From> for Value { + fn from(celled: Celled) -> Self { + match celled { + Celled::Value(value) => value.into(), + Celled::Func(func) => func.into(), + } + } +} diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index f2902b808..33b59d4db 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -1,8 +1,7 @@ -use crate::layout::{BlockNode, GridNode, HNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, GridLayouter, HNode, ParNode, Sizing, Spacing}; use crate::prelude::*; use crate::text::{SpaceNode, TextNode}; -/// # Term List /// A list of terms and their descriptions. /// /// Displays a sequence of terms and their descriptions vertically. When the @@ -20,55 +19,49 @@ use crate::text::{SpaceNode, TextNode}; /// between two adjacent letters. /// ``` /// -/// ## Parameters -/// - items: `Content` (positional, variadic) -/// The term list's children. -/// -/// When using the term list syntax, adjacent items are automatically -/// collected into term lists, even through constructs like for loops. -/// -/// ```example -/// #for year, product in ( -/// "1978": "TeX", -/// "1984": "LaTeX", -/// "2019": "Typst", -/// ) [/ #product: Born in #year.] -/// ``` -/// -/// - tight: `bool` (named) -/// If this is `{false}`, the items are spaced apart with [term list -/// spacing]($func/terms.spacing). If it is `{true}`, they use normal -/// [leading]($func/par.leading) instead. This makes the term list more -/// compact, which can look better if the items are short. -/// -/// ```example -/// / Fact: If a term list has a lot -/// of text, and maybe other inline -/// content, it should not be tight -/// anymore. -/// -/// / Tip: To make it wide, simply -/// insert a blank line between the -/// items. -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Term List +/// Category: layout +#[node(Layout)] pub struct TermsNode { - /// If true, the items are separated by leading instead of list spacing. - pub tight: bool, - /// The individual bulleted or numbered items. - pub items: StyleVec, -} + /// 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, + + /// 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 = Smart::Auto; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self { - tight: args.named("tight")?.unwrap_or(true), - items: args.all()?.into_iter().collect(), - } - .pack()) - } - - fn field(&self, name: &str) -> Option { - 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, } impl Layout for TermsNode { @@ -113,66 +92,63 @@ impl Layout for TermsNode { ) -> SourceResult { 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::().cloned().ok_or("expected term item or array")?, } diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs index 5d358e66d..2ab9e5e0a 100644 --- a/library/src/layout/transform.rs +++ b/library/src/layout/transform.rs @@ -2,7 +2,6 @@ use typst::geom::Transform; use crate::prelude::*; -/// # Move /// Move content without affecting layout. /// /// The `move` function allows you to move content while the layout still 'sees' @@ -22,39 +21,24 @@ use crate::prelude::*; /// )) /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to move. -/// -/// - dx: `Rel` (named) -/// The horizontal displacement of the content. -/// -/// - dy: `Rel` (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>, - /// 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 { - 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, + + /// The vertical displacement of the content. + #[named] + #[default] + pub dy: Rel, } impl Layout for MoveNode { @@ -65,15 +49,14 @@ impl Layout for MoveNode { regions: Regions, ) -> SourceResult { 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> = Axes::default(); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self { - angle: args.named_or_find("angle")?.unwrap_or_default(), - body: args.expect("body")?, - } - .pack()) - } + #[settable] + #[resolve] + #[default] + pub origin: Axes>, } impl Layout for RotateNode { @@ -150,18 +122,17 @@ impl Layout for RotateNode { regions: Regions, ) -> SourceResult { 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, - /// 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> = Axes::default(); + #[settable] + #[resolve] + #[default] + pub origin: Axes>, +} +impl Construct for ScaleNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { 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::("body")?).with_x(x).with_y(y).pack()) } } @@ -233,11 +199,11 @@ impl Layout for ScaleNode { regions: Regions, ) -> SourceResult { let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let transform = Transform::translate(x, y) - .pre_concat(Transform::scale(self.factor.x, self.factor.y)) + .pre_concat(Transform::scale(self.x(), self.y())) .pre_concat(Transform::translate(-x, -y)); frame.transform(transform); Ok(Fragment::frame(frame)) diff --git a/library/src/lib.rs b/library/src/lib.rs index 0759f73fd..e0994f25f 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -19,7 +19,7 @@ use self::layout::LayoutRoot; /// Construct the standard library. pub fn build() -> Library { let math = math::module(); - let calc = compute::calc(); + let calc = compute::calc::module(); let global = global(math.clone(), calc); Library { global, math, styles: styles(), items: items() } } @@ -166,37 +166,37 @@ fn items() -> LangItems { layout: |world, content, styles| content.layout_root(world, styles), em: |styles| styles.get(text::TextNode::SIZE), dir: |styles| styles.get(text::TextNode::DIR), - space: || text::SpaceNode.pack(), - linebreak: || text::LinebreakNode { justify: false }.pack(), - text: |text| text::TextNode(text).pack(), + space: || text::SpaceNode::new().pack(), + linebreak: || text::LinebreakNode::new().pack(), + text: |text| text::TextNode::new(text).pack(), text_id: NodeId::of::(), - text_str: |content| Some(&content.to::()?.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()), + smart_quote: |double| text::SmartQuoteNode::new().with_double(double).pack(), + parbreak: || layout::ParbreakNode::new().pack(), + strong: |body| text::StrongNode::new(body).pack(), + emph: |body| text::EmphNode::new(body).pack(), raw: |text, lang, block| { - let content = text::RawNode { text, block }.pack(); + let content = text::RawNode::new(text).with_block(block).pack(); match lang { Some(_) => content.styled(text::RawNode::LANG, lang), None => content, } }, link: |url| meta::LinkNode::from_url(url).pack(), - ref_: |target| meta::RefNode(target).pack(), - heading: |level, body| meta::HeadingNode { level, title: body }.pack(), - list_item: |body| layout::ListItem::List(body).pack(), - enum_item: |number, body| layout::ListItem::Enum(number, body).pack(), - term_item: |term, description| { - layout::ListItem::Term(layout::TermItem { term, description }).pack() + ref_: |target| meta::RefNode::new(target).pack(), + heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), + list_item: |body| layout::ListItem::new(body).pack(), + enum_item: |number, body| layout::EnumItem::new(body).with_number(number).pack(), + term_item: |term, description| layout::TermItem::new(term, description).pack(), + formula: |body, block| math::FormulaNode::new(body).with_block(block).pack(), + math_align_point: || math::AlignPointNode::new().pack(), + math_delimited: |open, body, close| math::LrNode::new(open + body + close).pack(), + math_attach: |base, bottom, top| { + math::AttachNode::new(base).with_bottom(bottom).with_top(top).pack() }, - formula: |body, block| math::FormulaNode { body, block }.pack(), - math_align_point: || math::AlignPointNode.pack(), - math_delimited: |open, body, close| { - math::LrNode { body: open + body + close, size: None }.pack() + math_accent: |base, accent| { + math::AccentNode::new(base, math::Accent::new(accent)).pack() }, - math_attach: |base, bottom, top| math::AttachNode { base, bottom, top }.pack(), - math_accent: |base, accent| math::AccentNode { base, accent }.pack(), - math_frac: |num, denom| math::FracNode { num, denom }.pack(), + math_frac: |num, denom| math::FracNode::new(num, denom).pack(), } } diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index 9c474eee7..164247de2 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -5,7 +5,6 @@ use super::*; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); -/// # Accent /// Attach an accent to a base. /// /// ## Example @@ -15,72 +14,48 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// $tilde(a) = accent(a, \u{0303})$ /// ``` /// -/// ## Parameters -/// - base: `Content` (positional, required) -/// The base to which the accent is applied. -/// May consist of multiple letters. -/// -/// ```example -/// $arrow(A B C)$ -/// ``` -/// -/// - accent: `char` (positional, required) -/// The accent to apply to the base. -/// -/// Supported accents include: -/// -/// | Accent | Name | Codepoint | -/// | ------------ | --------------- | --------- | -/// | Grave | `grave` | ` | -/// | 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 { - let base = args.expect("base")?; - let accent = args.expect::("accent")?.0; - Ok(Self { base, accent }.pack()) - } -} - -struct Accent(char); - -castable! { - Accent, - v: char => Self(v), - v: Content => match v.to::() { - 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` | ` | + /// | 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::() { + Some(node) => Value::Str(node.text().into()).cast()?, + None => Err("expected text")?, + }, +} + +cast_to_value! { + v: Accent => v.0.into() +} diff --git a/library/src/math/align.rs b/library/src/math/align.rs index a9005dfd7..6cf13a0ff 100644 --- a/library/src/math/align.rs +++ b/library/src/math/align.rs @@ -1,21 +1,11 @@ use super::*; -/// # Alignment Point /// A math alignment point: `&`, `&&`. /// -/// ## Parameters -/// - index: `usize` (positional, required) -/// The alignment point's index. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct AlignPointNode; - -#[node] -impl AlignPointNode {} +/// Display: Alignment Point +/// Category: math +#[node(LayoutMath)] +pub struct AlignPointNode {} impl LayoutMath for AlignPointNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs index 31dc75ab0..9181ab7c5 100644 --- a/library/src/math/attach.rs +++ b/library/src/math/attach.rs @@ -1,6 +1,5 @@ use super::*; -/// # Attachment /// A base with optional attachments. /// /// ## Syntax @@ -12,58 +11,44 @@ use super::*; /// $ sum_(i=0)^n a_i = 2^(1+i) $ /// ``` /// -/// ## Parameters -/// - base: `Content` (positional, required) -/// The base to which things are attached. -/// -/// - top: `Content` (named) -/// The top attachment. -/// -/// - bottom: `Content` (named) -/// The bottom attachment. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] +/// Display: Attachment +/// Category: math +#[node(LayoutMath)] pub struct AttachNode { - /// The base. + /// The base to which things are attached. + #[positional] + #[required] pub base: Content, - /// The top attachment. - pub top: Option, - /// The bottom attachment. - pub bottom: Option, -} -#[node] -impl AttachNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, + + /// The bottom attachment. + #[named] + #[default] + pub bottom: Option, } 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::(); + let display_scripts = base.is::(); + + 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::() - || (!self.base.is::() + 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 { - 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 { - Ok(Self(args.expect("base")?).pack()) - } +/// Display: Limits +/// Category: math +#[node(LayoutMath)] +pub struct LimitsNode { + /// The base to attach the limits to. + #[positional] + #[required] + pub base: Content, } impl LayoutMath for LimitsNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - self.0.layout_math(ctx) + self.base().layout_math(ctx) } } diff --git a/library/src/math/delimited.rs b/library/src/math/delimited.rs index 511f1a7be..b0126cadd 100644 --- a/library/src/math/delimited.rs +++ b/library/src/math/delimited.rs @@ -3,7 +3,6 @@ use super::*; /// How much less high scaled delimiters can be than what they wrap. pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); -/// # Left/Right /// Scales delimiters. /// /// While matched delimiters scale by default, this can be used to scale @@ -24,20 +23,22 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); /// /// Defaults to `{100%}`. /// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] +/// Display: Left/Right +/// Category: math +#[node(Construct, LayoutMath)] pub struct LrNode { /// The delimited content, including the delimiters. + #[positional] + #[required] pub body: Content, + /// The size of the brackets. - pub size: Option>, + #[named] + #[default] + pub size: Smart>, } -#[node] -impl LrNode { +impl Construct for LrNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let mut body = Content::empty(); for (i, arg) in args.all::()?.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::>>("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::() { - if node.size.is_none() { - body = &node.body; + let mut body = self.body(); + if let Some(node) = body.to::() { + 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 { delimited(args, '⌊', '⌋') } -/// # Ceil /// Ceil an expression. /// /// ## Example @@ -147,14 +146,13 @@ pub fn floor(args: &mut Args) -> SourceResult { /// - body: `Content` (positional, required) /// The expression to ceil. /// -/// ## Category -/// math +/// Display: Ceil +/// Category: math #[func] pub fn ceil(args: &mut Args) -> SourceResult { delimited(args, '⌈', '⌉') } -/// # Abs /// Take the absolute value of an expression. /// /// ## Example @@ -166,14 +164,13 @@ pub fn ceil(args: &mut Args) -> SourceResult { /// - 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 { delimited(args, '|', '|') } -/// # Norm /// Take the norm of an expression. /// /// ## Example @@ -185,8 +182,8 @@ pub fn abs(args: &mut Args) -> SourceResult { /// - 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 { delimited(args, '‖', '‖') @@ -194,14 +191,11 @@ pub fn norm(args: &mut Args) -> SourceResult { fn delimited(args: &mut Args, left: char, right: char) -> SourceResult { Ok(Value::Content( - LrNode { - body: Content::sequence(vec![ - TextNode::packed(left), - args.expect::("body")?, - TextNode::packed(right), - ]), - size: None, - } + LrNode::new(Content::sequence(vec![ + TextNode::packed(left), + args.expect::("body")?, + TextNode::packed(right), + ])) .pack(), )) } diff --git a/library/src/math/frac.rs b/library/src/math/frac.rs index cdf533da2..ea647fc52 100644 --- a/library/src/math/frac.rs +++ b/library/src/math/frac.rs @@ -17,41 +17,27 @@ const FRAC_AROUND: Em = Em::new(0.1); /// expression using round grouping parenthesis. Such parentheses are removed /// from the output, but you can nest multiple to force them. /// -/// ## Parameters -/// - num: `Content` (positional, required) -/// The fraction's numerator. -/// -/// - denom: `Content` (positional, required) -/// The fraction's denominator. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] +/// Display: Fraction +/// Category: math +#[node(LayoutMath)] pub struct FracNode { - /// The numerator. + /// The fraction's numerator. + #[positional] + #[required] pub num: Content, - /// The denominator. - pub denom: Content, -} -#[node] -impl FracNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - let upper = args.expect("upper index")?; - let lower = args.expect("lower index")?; - Ok(Self { upper, lower }.pack()) - } + /// The binomial's lower index. + #[positional] + #[required] + pub lower: Content, } impl LayoutMath for BinomNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, &self.upper, &self.lower, true) + layout(ctx, &self.upper(), &self.lower(), true) } } diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs index 978d262b4..3e2573853 100644 --- a/library/src/math/matrix.rs +++ b/library/src/math/matrix.rs @@ -4,7 +4,6 @@ const ROW_GAP: Em = Em::new(0.5); const COL_GAP: Em = Em::new(0.5); const VERTICAL_PADDING: Ratio = Ratio::new(0.1); -/// # Vector /// A column vector. /// /// Content in the vector's elements can be aligned with the `&` symbol. @@ -15,41 +14,33 @@ const VERTICAL_PADDING: Ratio = Ratio::new(0.1); /// = a + 2b + 3c $ /// ``` /// -/// ## Parameters -/// - elements: `Content` (positional, variadic) -/// The elements of the vector. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct VecNode(Vec); +/// Display: Vector +/// Category: math +#[node(LayoutMath)] +pub struct VecNode { + /// The elements of the vector. + #[variadic] + pub elements: Vec, -#[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 { - 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>); +/// 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>, -#[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 { 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); +/// Display: Cases +/// Category: math +#[node(LayoutMath)] +pub struct CasesNode { + /// The branches of the case distinction. + #[variadic] + pub branches: Vec, -#[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 { - Ok(Self(args.all()?).pack()) - } + #[settable] + #[default(Delimiter::Brace)] + pub delim: Delimiter, } impl LayoutMath for CasesNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { let delim = ctx.styles().get(Self::DELIM); - let frame = layout_vec_body(ctx, &self.0, Align::Left)?; + let frame = layout_vec_body(ctx, &self.branches(), Align::Left)?; layout_delimiters(ctx, frame, Some(delim.open()), None) } } @@ -214,7 +196,7 @@ impl Delimiter { } } -castable! { +cast_from_value! { Delimiter, /// Delimit with parentheses. "(" => Self::Paren, @@ -228,6 +210,16 @@ castable! { "||" => Self::DoubleBar, } +cast_to_value! { + v: Delimiter => Value::from(match v { + Delimiter::Paren => "(", + Delimiter::Bracket => "[", + Delimiter::Brace => "{", + Delimiter::Bar => "|", + Delimiter::DoubleBar => "||", + }) +} + /// Layout the inner contents of a vector. fn layout_vec_body( ctx: &mut MathContext, diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index d73b17690..9da12e4f4 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -107,7 +107,6 @@ pub fn module() -> Module { Module::new("math").with_scope(math) } -/// # Formula /// A mathematical formula. /// /// Can be displayed inline with text or as a separate block. @@ -132,46 +131,25 @@ pub fn module() -> Module { /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The contents of the formula. -/// -/// - block: `bool` (named) -/// Whether the formula is displayed as a separate block. -/// -/// ## Category -/// math -#[func] -#[capable(Show, Finalize, Layout, LayoutMath)] -#[derive(Debug, Clone, Hash)] +/// Display: Formula +/// Category: math +#[node(Show, Finalize, Layout, LayoutMath)] pub struct FormulaNode { - /// Whether the formula is displayed as a separate block. - pub block: bool, /// The content of the formula. + #[positional] + #[required] pub body: Content, -} -#[node] -impl FormulaNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - let body = args.expect("body")?; - let block = args.named("block")?.unwrap_or(false); - Ok(Self { block, body }.pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { let mut realized = self.clone().pack().guarded(Guard::Base(NodeId::of::())); - 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 { + 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::() { - for child in &node.0 { + for child in node.children() { child.layout_math(ctx)?; } return Ok(()); } if let Some(styled) = self.to::() { - 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::() { - 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::() { - ctx.layout_text(&node.0)?; + ctx.layout_text(&node.text())?; return Ok(()); } diff --git a/library/src/math/op.rs b/library/src/math/op.rs index 4eb9c48c3..c855cd92e 100644 --- a/library/src/math/op.rs +++ b/library/src/math/op.rs @@ -2,7 +2,6 @@ use typst::eval::Scope; use super::*; -/// # Text Operator /// A text operator in a math formula. /// /// ## Example @@ -19,45 +18,30 @@ use super::*; /// `max`, `min`, `Pr`, `sec`, `sin`, `sinh`, `sup`, `tan`, `tg`, `tanh`, /// `liminf`, and `limsup`. /// -/// ## Parameters -/// - text: `EcoString` (positional, required) -/// The operator's text. -/// -/// - limits: `bool` (named) -/// Whether the operator should force attachments to display as limits. -/// -/// Defaults to `{false}`. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] +/// Display: Text Operator +/// Category: math +#[node(LayoutMath)] pub struct OpNode { /// The operator's text. + #[positional] + #[required] pub text: EcoString, - /// Whether the operator should force attachments to display as limits. - pub limits: bool, -} -#[node] -impl OpNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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')); } diff --git a/library/src/math/root.rs b/library/src/math/root.rs index e40f56f04..191acb94a 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -1,6 +1,5 @@ use super::*; -/// # Square Root /// A square root. /// /// ## Example @@ -8,31 +7,22 @@ use super::*; /// $ sqrt(x^2) = x = sqrt(x)^2 $ /// ``` /// -/// ## Parameters -/// - radicand: `Content` (positional, required) -/// The expression to take the square root of. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct SqrtNode(pub Content); - -#[node] -impl SqrtNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - 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 { let node = index?.to::()?; - let c = match node.0.as_str() { + let c = match node.text().as_str() { "3" => '∛', "4" => '∜', _ => return None, diff --git a/library/src/math/row.rs b/library/src/math/row.rs index 1271e49e6..b7720c141 100644 --- a/library/src/math/row.rs +++ b/library/src/math/row.rs @@ -103,7 +103,7 @@ impl MathRow { pub fn to_frame(self, ctx: &MathContext) -> Frame { let styles = ctx.styles(); - let align = styles.get(AlignNode::ALIGNS).x.resolve(styles); + let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles); self.to_aligned_frame(ctx, &[], align) } @@ -200,10 +200,7 @@ impl MathRow { } } -impl From for MathRow -where - T: Into, -{ +impl> From for MathRow { fn from(fragment: T) -> Self { Self(vec![fragment.into()]) } diff --git a/library/src/math/spacing.rs b/library/src/math/spacing.rs index 172672388..e1b9d4084 100644 --- a/library/src/math/spacing.rs +++ b/library/src/math/spacing.rs @@ -7,10 +7,10 @@ pub(super) const QUAD: Em = Em::new(1.0); /// Hook up all spacings. pub(super) fn define(math: &mut Scope) { - math.define("thin", HNode::strong(THIN).pack()); - math.define("med", HNode::strong(MEDIUM).pack()); - math.define("thick", HNode::strong(THICK).pack()); - math.define("quad", HNode::strong(QUAD).pack()); + math.define("thin", HNode::new(THIN.into()).pack()); + math.define("med", HNode::new(MEDIUM.into()).pack()); + math.define("thick", HNode::new(THICK.into()).pack()); + math.define("quad", HNode::new(QUAD.into()).pack()); } /// Create the spacing between two fragments in a given style. diff --git a/library/src/math/style.rs b/library/src/math/style.rs index 9856a6b08..993651065 100644 --- a/library/src/math/style.rs +++ b/library/src/math/style.rs @@ -1,6 +1,5 @@ use super::*; -/// # Bold /// Bold font style in math. /// /// ## Example @@ -8,34 +7,25 @@ use super::*; /// $ bold(A) := B^+ $ /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The piece of formula to style. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct BoldNode(pub Content); - -#[node] -impl BoldNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - Ok(Self(args.expect("body")?).pack()) - } +/// Display: Blackboard Bold +/// Category: math +#[node(LayoutMath)] +pub struct BbNode { + /// The piece of formula to style. + #[positional] + #[required] + pub body: Content, } impl LayoutMath for BbNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { ctx.style(ctx.style.with_variant(MathVariant::Bb)); - self.0.layout_math(ctx)?; + self.body().layout_math(ctx)?; ctx.unstyle(); Ok(()) } diff --git a/library/src/math/underover.rs b/library/src/math/underover.rs index 4387de201..87f30c0ff 100644 --- a/library/src/math/underover.rs +++ b/library/src/math/underover.rs @@ -4,7 +4,6 @@ const LINE_GAP: Em = Em::new(0.15); const BRACE_GAP: Em = Em::new(0.25); const BRACKET_GAP: Em = Em::new(0.25); -/// # Underline /// A horizontal line under content. /// /// ## Example @@ -12,31 +11,22 @@ const BRACKET_GAP: Em = Em::new(0.25); /// $ underline(1 + 2 + ... + 5) $ /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content above the line. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct UnderlineNode(Content); - -#[node] -impl UnderlineNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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 { - 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, -} -#[node] -impl UnderbraceNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, } 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, -} -#[node] -impl OverbraceNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, } 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, -} -#[node] -impl UnderbracketNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, } 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, -} -#[node] -impl OverbracketNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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, } impl LayoutMath for OverbracketNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, &self.body, &self.annotation, '⎴', BRACKET_GAP, true) + layout(ctx, &self.body(), &self.annotation(), '⎴', BRACKET_GAP, true) } } diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index 1d349b89e..0d03b4969 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -1,7 +1,8 @@ +use typst::model::StyledNode; + use crate::layout::{LayoutRoot, PageNode}; use crate::prelude::*; -/// # Document /// The root element of a document and its metadata. /// /// All documents are automatically wrapped in a `document` element. The main @@ -11,33 +12,48 @@ use crate::prelude::*; /// The metadata set with this function is not rendered within the document. /// Instead, it is embedded in the compiled PDF file. /// -/// ## Category -/// meta -#[func] -#[capable(LayoutRoot)] -#[derive(Hash)] -pub struct DocumentNode(pub StyleVec); +/// Display: Document +/// Category: meta +#[node(LayoutRoot)] +pub struct DocumentNode { + /// The page runs. + #[variadic] + pub children: Vec, -#[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 = None; + #[settable] + #[default] + pub title: Option, /// 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 { 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::() { + map = node.map(); + styles = outer.chain(&map); + child = node.sub(); + } + + if let Some(page) = child.to::() { + 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); -castable! { +cast_from_value! { Author, v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), } + +cast_to_value! { + v: Author => v.0.into() +} diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index f108cad16..38890885c 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -5,7 +5,6 @@ use crate::layout::{BlockNode, HNode, VNode}; use crate::prelude::*; use crate::text::{TextNode, TextSize}; -/// # Heading /// A section heading. /// /// With headings, you can structure your document into sections. Each heading @@ -39,28 +38,20 @@ use crate::text::{TextNode, TextSize}; /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. /// -/// ## Parameters -/// - title: `Content` (positional, required) -/// The heading's title. -/// -/// - level: `NonZeroUsize` (named) -/// The logical nesting depth of the heading, starting from one. -/// -/// ## Category -/// meta -#[func] -#[capable(Prepare, Show, Finalize)] -#[derive(Debug, Hash)] +/// Display: Heading +/// Category: meta +#[node(Prepare, Show, Finalize)] pub struct HeadingNode { - /// The logical nesting depth of the section, starting from one. In the - /// default style, this controls the text size of the heading. - pub level: NonZeroUsize, - /// The heading's contents. + /// The heading's title. + #[positional] + #[required] pub title: Content, -} -#[node] -impl HeadingNode { + /// The logical nesting depth of the heading, starting from one. + #[named] + #[default(NonZeroUsize::new(1).unwrap())] + pub level: NonZeroUsize, + /// How to number the heading. Accepts a /// [numbering pattern or function]($func/numbering). /// @@ -71,8 +62,9 @@ impl HeadingNode { /// == A subsection /// === A sub-subsection /// ``` - #[property(referenced)] - pub const NUMBERING: Option = None; + #[settable] + #[default] + pub numbering: Option, /// 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 { - Ok(Self { - title: args.expect("title")?, - level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), - } - .pack()) - } - - fn field(&self, name: &str) -> Option { - 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::().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 { - let mut realized = self.title.clone(); + let mut realized = self.title(); let numbers = this.field("numbers").unwrap(); - if numbers != Value::None { - realized = numbers.display() - + HNode { amount: Em::new(0.3).into(), weak: true }.pack() + if *numbers != Value::None { + realized = numbers.clone().display() + + HNode::new(Em::new(0.3).into()).with_weak(true).pack() + realized; } - Ok(BlockNode { - body: realized, - width: Smart::Auto, - height: Smart::Auto, - } - .pack()) + Ok(BlockNode::new().with_body(realized).pack()) } } impl Finalize for HeadingNode { fn finalize(&self, realized: Content) -> Content { - let scale = match self.level.get() { + let level = self.level().get(); + let scale = match level { 1 => 1.4, 2 => 1.2, _ => 1.0, }; let size = Em::new(scale); - let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; + let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale; let below = Em::new(0.75) / scale; let mut map = StyleMap::new(); @@ -191,7 +165,7 @@ impl HeadingCounter { /// Advance the counter and return the numbers for the given heading. pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] { - let level = heading.level.get(); + let level = heading.level().get(); if self.0.len() >= level { self.0[level - 1] = self.0[level - 1].saturating_add(1); diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index 61e75b8a5..63a8ec98b 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::{Hyphenate, TextNode}; -/// # Link /// Link to a URL or another location in the document. /// /// The link function makes its positional `body` argument clickable and links @@ -50,67 +49,62 @@ use crate::text::{Hyphenate, TextNode}; /// The content that should become a link. If `dest` is an URL string, the /// parameter can be omitted. In this case, the URL will be shown as the link. /// -/// ## Category -/// meta -#[func] -#[capable(Show, Finalize)] -#[derive(Debug, Hash)] +/// Display: Link +/// Category: meta +#[node(Construct, Show, Finalize)] pub struct LinkNode { /// The destination the link points to. + #[positional] + #[required] pub dest: Destination, + /// How the link is represented. + #[positional] + #[default] pub body: Content, } impl LinkNode { /// Create a link node from a URL with its bare text. pub fn from_url(url: EcoString) -> Self { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - let shorter = text.len() < url.len(); - let body = TextNode::packed(if shorter { text.into() } else { url.clone() }); - Self { dest: Destination::Url(url), body } + let body = body_from_url(&url); + Self::new(Destination::Url(url)).with_body(body) } } -#[node] -impl LinkNode { +impl Construct for LinkNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let dest = args.expect::("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 { - 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 { - Ok(self.body.clone()) + Ok(self.body()) } } impl Finalize for LinkNode { fn finalize(&self, realized: Content) -> Content { realized - .styled(Meta::DATA, vec![Meta::Link(self.dest.clone())]) + .styled(MetaNode::DATA, vec![Meta::Link(self.dest())]) .styled(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false))) } } + +fn body_from_url(url: &EcoString) -> Content { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + let shorter = text.len() < url.len(); + TextNode::packed(if shorter { text.into() } else { url.clone() }) +} diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index 5b7cd92b8..d3e1ee4d5 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use crate::prelude::*; use crate::text::Case; -/// # Numbering /// Apply a numbering to a sequence of numbers. /// /// A numbering defines how a sequence of numbers should be displayed as @@ -61,8 +60,8 @@ use crate::text::Case; /// /// - returns: any /// -/// ## Category -/// meta +/// Display: Numbering +/// Category: meta #[func] pub fn numbering(vm: &Vm, args: &mut Args) -> SourceResult { let numbering = args.expect::("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 { + Some(match c { + '1' => NumberingKind::Arabic, + 'a' => NumberingKind::Letter, + 'i' => NumberingKind::Roman, + '*' => NumberingKind::Symbol, + _ => return None, + }) + } + + /// The lowercase character for this numbering kind. + pub fn to_char(self) -> char { + match self { + Self::Arabic => '1', + Self::Letter => 'a', + Self::Roman => 'i', + Self::Symbol => '*', + } + } + /// Apply the numbering to the given number. pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString { let mut n = n.get(); diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index d9eea0a90..7bc08bf99 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -1,11 +1,8 @@ use super::HeadingNode; -use crate::layout::{ - BoxNode, HNode, HideNode, ParbreakNode, RepeatNode, Sizing, Spacing, -}; +use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode}; use crate::prelude::*; use crate::text::{LinebreakNode, SpaceNode, TextNode}; -/// # Outline /// A section outline / table of contents. /// /// This function generates a list of all headings in the document, up to a @@ -23,27 +20,25 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode}; /// #lorem(10) /// ``` /// -/// ## Category -/// meta -#[func] -#[capable(Prepare, Show)] -#[derive(Debug, Hash)] -pub struct OutlineNode; - -#[node] -impl OutlineNode { +/// Display: Outline +/// Category: meta +#[node(Prepare, Show)] +pub struct OutlineNode { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the [text /// language]($func/text.lang) will be used. This is the default. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. - #[property(referenced)] - pub const TITLE: Option> = Some(Smart::Auto); + #[settable] + #[default(Some(Smart::Auto))] + pub title: Option>, /// 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 = None; + #[settable] + #[default] + pub depth: Option, /// 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 = Some(RepeatNode(TextNode::packed(".")).pack()); - - fn construct(_: &Vm, _: &mut Args) -> SourceResult { - Ok(Self.pack()) - } + #[settable] + #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))] + pub fill: Option, } impl Prepare for OutlineNode { @@ -91,7 +85,7 @@ impl Prepare for OutlineNode { .locate(Selector::node::()) .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 { - 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::()) { - if node.field("outlined").unwrap() != Value::Bool(true) { + if *node.field("outlined").unwrap() != Value::Bool(true) { continue; } let heading = node.to::().unwrap(); if let Some(depth) = depth { - if depth < heading.level { + if depth < heading.level() { continue; } } while ancestors.last().map_or(false, |last| { - last.to::().unwrap().level >= heading.level + last.to::().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::().unwrap(); + let mut loc = node.field("loc").unwrap().clone().cast::().unwrap(); loc.pos -= Point::splat(Abs::pt(10.0)); // Add hidden ancestors numberings to realize the indent. @@ -156,21 +150,21 @@ impl Show for OutlineNode { let hidden: Vec<_> = ancestors .iter() .map(|node| node.field("numbers").unwrap()) - .filter(|numbers| *numbers != Value::None) - .map(|numbers| numbers.display() + SpaceNode.pack()) + .filter(|&numbers| *numbers != Value::None) + .map(|numbers| numbers.clone().display() + SpaceNode::new().pack()) .collect(); if !hidden.is_empty() { - seq.push(HideNode(Content::sequence(hidden)).pack()); - seq.push(SpaceNode.pack()); + seq.push(HideNode::new(Content::sequence(hidden)).pack()); + seq.push(SpaceNode::new().pack()); } } // Format the numbering. - let mut start = heading.title.clone(); + let mut start = heading.title(); let numbers = node.field("numbers").unwrap(); - if numbers != Value::None { - start = numbers.display() + SpaceNode.pack() + start; + if *numbers != Value::None { + start = numbers.clone().display() + SpaceNode::new().pack() + start; }; // Add the numbering and section name. @@ -178,30 +172,27 @@ impl Show for OutlineNode { // Add filler symbols between the section name and page number. if let Some(filler) = styles.get(Self::FILL) { - seq.push(SpaceNode.pack()); + seq.push(SpaceNode::new().pack()); seq.push( - BoxNode { - body: filler.clone(), - width: Sizing::Fr(Fr::one()), - height: Smart::Auto, - } - .pack(), + BoxNode::new() + .with_body(filler.clone()) + .with_width(Fr::one().into()) + .pack(), ); - seq.push(SpaceNode.pack()); + seq.push(SpaceNode::new().pack()); } else { - let amount = Spacing::Fr(Fr::one()); - seq.push(HNode { amount, weak: false }.pack()); + seq.push(HNode::new(Fr::one().into()).pack()); } // Add the page number and linebreak. let end = TextNode::packed(eco_format!("{}", loc.page)); seq.push(end.linked(Destination::Internal(loc))); - seq.push(LinebreakNode { justify: false }.pack()); + seq.push(LinebreakNode::new().pack()); ancestors.push(node); } - seq.push(ParbreakNode.pack()); + seq.push(ParbreakNode::new().pack()); Ok(Content::sequence(seq)) } diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index e64751f74..55051b5e4 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::TextNode; -/// # Reference /// A reference to a label. /// /// *Note: This function is currently unimplemented.* @@ -16,33 +15,18 @@ use crate::text::TextNode; /// created by typing an `@` followed by the name of the label (e.g. `[= /// Introduction ]` 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 { - Ok(Self(args.expect("target")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - Ok(TextNode::packed(eco_format!("@{}", self.0))) + Ok(TextNode::packed(eco_format!("@{}", self.target()))) } } diff --git a/library/src/prelude.rs b/library/src/prelude.rs index a406e5867..49afc9ca9 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -15,16 +15,16 @@ pub use typst::diag::{bail, error, At, SourceResult, StrResult}; pub use typst::doc::*; #[doc(no_inline)] pub use typst::eval::{ - array, castable, dict, format_str, func, Args, Array, AutoValue, Cast, CastInfo, - Dict, Func, NoneValue, Str, Symbol, Value, Vm, + array, cast_from_value, cast_to_value, dict, format_str, func, Args, Array, Cast, + CastInfo, Dict, Func, Never, Str, Symbol, Value, Vm, }; #[doc(no_inline)] pub use typst::geom::*; #[doc(no_inline)] pub use typst::model::{ - capability, capable, node, Content, Finalize, Fold, Introspector, Label, Node, - NodeId, Prepare, Resolve, Selector, Show, StabilityProvider, StyleChain, StyleMap, - StyleVec, Unlabellable, Vt, + node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Prepare, + Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec, + Unlabellable, Vt, }; #[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; diff --git a/library/src/shared/behave.rs b/library/src/shared/behave.rs index ec8fade91..74c4d151f 100644 --- a/library/src/shared/behave.rs +++ b/library/src/shared/behave.rs @@ -1,9 +1,8 @@ //! Node interaction. -use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder}; +use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder}; /// How a node interacts with other nodes. -#[capability] pub trait Behave { /// The node's interaction behaviour. fn behaviour(&self) -> Behaviour; @@ -23,7 +22,7 @@ pub enum Behaviour { /// after it. Furthermore, per consecutive run of weak nodes, only one /// survives: The one with the lowest weakness level (or the larger one if /// there is a tie). - Weak(u8), + Weak(usize), /// A node that enables adjacent weak nodes to exist. The default. Supportive, /// A node that destroys adjacent weak nodes. diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs index 3e600b8de..83e62ac47 100644 --- a/library/src/shared/ext.rs +++ b/library/src/shared/ext.rs @@ -24,49 +24,43 @@ pub trait ContentExt { /// Transform this content's contents without affecting layout. fn moved(self, delta: Axes>) -> 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>) -> Self { - self.styled(crate::layout::AlignNode::ALIGNS, aligns) + self.styled(crate::layout::AlignNode::ALIGNMENT, aligns) } fn padded(self, padding: Sides>) -> 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>) -> 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 { - 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 { - let mut fragment = self.child.layout(vt, styles, regions)?; - for frame in &mut fragment { - let shape = Geometry::Rect(frame.size()).stroked(self.stroke); - frame.prepend(Point::zero(), Element::Shape(shape)); - } - Ok(fragment) - } -} diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index 4043141bb..18145d28e 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -4,7 +4,6 @@ use ttf_parser::{GlyphId, OutlineBuilder}; use super::TextNode; use crate::prelude::*; -/// # Underline /// Underline text. /// /// ## Example @@ -12,19 +11,15 @@ use crate::prelude::*; /// This is #underline[important]. /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to underline. -/// -/// ## Category -/// text -#[func] -#[capable(Show)] -#[derive(Debug, Hash)] -pub struct UnderlineNode(pub Content); +/// 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 = Smart::Auto; + #[settable] + #[shorthand] + #[resolve] + #[fold] + #[default] + pub stroke: Smart, /// 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 = Smart::Auto; + #[settable] + #[resolve] + #[default] + pub offset: Smart, /// 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 { - Ok(Self(args.expect("body")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - 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 = Smart::Auto; + #[settable] + #[shorthand] + #[resolve] + #[fold] + #[default] + pub stroke: Smart, /// 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 = Smart::Auto; + #[settable] + #[resolve] + #[default] + pub offset: Smart, /// 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 { - Ok(Self(args.expect("body")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - 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 = Smart::Auto; + #[settable] + #[shorthand] + #[resolve] + #[fold] + #[default] + pub stroke: Smart, /// 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 = Smart::Auto; + #[settable] + #[resolve] + #[default] + pub offset: Smart, /// 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 { - Ok(Self(args.expect("body")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - Ok(self.0.clone().styled( + Ok(self.body().styled( TextNode::DECO, Decoration { line: DecoLine::Strikethrough, @@ -294,6 +274,10 @@ impl Fold for Decoration { } } +cast_from_value! { + Decoration: "decoration", +} + /// A kind of decorative line. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum DecoLine { diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs index de7974cd9..9caaf68e5 100644 --- a/library/src/text/misc.rs +++ b/library/src/text/misc.rs @@ -1,24 +1,12 @@ use super::TextNode; use crate::prelude::*; -/// # Space /// A text space. /// -/// ## Category -/// text -#[func] -#[capable(Unlabellable, Behave)] -#[derive(Debug, Hash)] -pub struct SpaceNode; - -#[node] -impl SpaceNode { - fn construct(_: &Vm, _: &mut Args) -> SourceResult { - 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 { - 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 { - Ok(Self(args.expect("body")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - 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 { - Ok(Self(args.expect("body")?).pack()) - } - - fn field(&self, name: &str) -> Option { - 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 { - 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 { case(Case::Lower, args) } -/// # Uppercase /// Convert text or content to uppercase. /// /// ## Example @@ -263,8 +222,8 @@ pub fn lower(args: &mut Args) -> SourceResult { /// - text: `ToCase` (positional, required) /// The text to convert to uppercase. /// -/// ## Category -/// text +/// Display: Uppercase +/// Category: text #[func] pub fn upper(args: &mut Args) -> SourceResult { case(Case::Upper, args) @@ -272,21 +231,22 @@ pub fn upper(args: &mut Args) -> SourceResult { /// Change the case of text. fn case(case: Case, args: &mut Args) -> SourceResult { - 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 { 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 { /// /// - returns: string /// -/// ## Category -/// text +/// Display: Blind Text +/// Category: text #[func] pub fn lorem(args: &mut Args) -> SourceResult { let words: usize = args.expect("number of words")?; diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index bdd2d0c21..cde0163ef 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -22,7 +22,6 @@ use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontM use crate::layout::ParNode; use crate::prelude::*; -/// # Text /// Customize the look and layout of text in a variety of ways. /// /// This function is used often, both with set rules and directly. While the set @@ -62,26 +61,50 @@ use crate::prelude::*; /// - body: `Content` (positional, required) /// Content in which all text is styled according to the other arguments. /// -/// ## Category -/// text -#[func] -#[capable] -#[derive(Clone, Hash)] -pub struct TextNode(pub EcoString); +/// Display: Text +/// Category: text +#[node(Construct)] +#[set({ + if let Some(family) = args.named("family")? { + styles.set(Self::FAMILY, family); + } else { + let mut count = 0; + let mut content = false; + for item in args.items.iter().filter(|item| item.name.is_none()) { + if EcoString::is(&item.value) { + count += 1; + } else if >>::is(&item.value) { + content = true; + } + } -impl TextNode { - /// Create a new packed text node. - pub fn packed(text: impl Into) -> 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 = Rel::one(); + #[settable] + #[resolve] + #[default(Rel::one())] + pub spacing: Rel, /// 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 = None; + #[settable] + #[default(None)] + pub region: Option, /// 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 = None; + #[settable] + #[default(None)] + pub stylistic_set: Option, /// 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 = Smart::Auto; + #[settable] + #[default(Smart::Auto)] + pub number_type: Smart, /// 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 = Smart::Auto; + #[settable] + #[default(Smart::Auto)] + pub number_width: Smart, /// 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 = 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, + + /// 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) -> Content { + Self::new(text.into()).pack() + } +} + +impl Construct for TextNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { // 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 >>::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 { - 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); -castable! { +cast_from_value! { FallbackList, family: FontFamily => Self(vec![family]), values: Array => Self(values.into_iter().map(|v| v.cast()).collect::>()?), } +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); -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 => { + 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); -castable! { +cast_from_value! { Hyphenate, - _: AutoValue => Self(Smart::Auto), - v: bool => Self(Smart::Custom(v)), + v: Smart => 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::>()?), } +cast_to_value! { + v: FontFeatures => Value::Dict( + v.0.into_iter() + .map(|(tag, num)| { + let bytes = tag.to_bytes(); + let key = std::str::from_utf8(&bytes).unwrap_or_default(); + (key.into(), num.into()) + }) + .collect(), + ) +} + impl Fold for FontFeatures { type Output = Self; diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs index c0a7e11c4..1c6028712 100644 --- a/library/src/text/quotes.rs +++ b/library/src/text/quotes.rs @@ -2,7 +2,6 @@ use typst::syntax::is_newline; use crate::prelude::*; -/// # Smart Quote /// A language-aware quote that reacts to its context. /// /// Automatically turns into an appropriate opening or closing quote based on @@ -23,21 +22,15 @@ use crate::prelude::*; /// This function also has dedicated syntax: The normal quote characters /// (`'` and `"`). Typst automatically makes your quotes smart. /// -/// ## Parameters -/// - double: `bool` (named) -/// Whether this should be a double quote. -/// -/// ## Category -/// text -#[func] -#[capable] -#[derive(Debug, Hash)] -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 { - let double = args.named("double")?.unwrap_or(true); - Ok(Self { double }.pack()) - } - - fn field(&self, name: &str) -> Option { - match name { - "double" => Some(Value::Bool(self.double)), - _ => None, - } - } + #[settable] + #[default(true)] + pub enabled: bool, } /// State machine for smart quote substitution. diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index ec11582ce..cdaefd060 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -9,7 +9,6 @@ use super::{ use crate::layout::BlockNode; use crate::prelude::*; -/// # Raw Text / Code /// Raw text with optional syntax highlighting. /// /// Displays the text verbatim and in a monospace font. This is typically used @@ -35,71 +34,64 @@ use crate::prelude::*; /// ``` /// ```` /// -/// ## Parameters -/// - text: `EcoString` (positional, required) -/// The raw text. -/// -/// You can also use raw blocks creatively to create custom syntaxes for -/// your automations. -/// -/// ````example -/// // Parse numbers in raw blocks with the -/// // `mydsl` tag and sum them up. -/// #show raw.where(lang: "mydsl"): it => { -/// let sum = 0 -/// for part in it.text.split("+") { -/// sum += int(part.trim()) -/// } -/// sum -/// } -/// -/// ```mydsl -/// 1 + 2 + 3 + 4 + 5 -/// ``` -/// ```` -/// -/// - block: `bool` (named) -/// Whether the raw text is displayed as a separate block. -/// -/// ````example -/// // Display inline code in a small box -/// // that retains the correct baseline. -/// #show raw.where(block: false): box.with( -/// fill: luma(240), -/// inset: (x: 3pt, y: 0pt), -/// outset: (y: 3pt), -/// radius: 2pt, -/// ) -/// -/// // Display block code in a larger block -/// // with more padding. -/// #show raw.where(block: true): block.with( -/// fill: luma(240), -/// inset: 10pt, -/// radius: 4pt, -/// ) -/// -/// With `rg`, you can search through your files quickly. -/// -/// ```bash -/// rg "Hello World" -/// ``` -/// ```` -/// -/// ## Category -/// text -#[func] -#[capable(Prepare, Show, Finalize)] -#[derive(Debug, Hash)] +/// Display: Raw Text / Code +/// Category: text +#[node(Prepare, Show, Finalize)] pub struct RawNode { /// The raw text. + /// + /// You can also use raw blocks creatively to create custom syntaxes for + /// your automations. + /// + /// ````example + /// // Parse numbers in raw blocks with the + /// // `mydsl` tag and sum them up. + /// #show raw.where(lang: "mydsl"): it => { + /// let sum = 0 + /// for part in it.text.split("+") { + /// sum += int(part.trim()) + /// } + /// sum + /// } + /// + /// ```mydsl + /// 1 + 2 + 3 + 4 + 5 + /// ``` + /// ```` + #[positional] + #[required] pub text: EcoString, - /// Whether the raw text is displayed as a separate block. - 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 = None; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self { - text: args.expect("text")?, - block: args.named("block")?.unwrap_or(false), - } - .pack()) - } - - fn field(&self, name: &str) -> Option { - match name { - "text" => Some(Value::Str(self.text.clone().into())), - "block" => Some(Value::Bool(self.block)), - _ => None, - } - } + #[settable] + #[default] + pub lang: Option, } impl Prepare for RawNode { @@ -138,19 +115,14 @@ impl Prepare for RawNode { mut this: Content, styles: StyleChain, ) -> SourceResult { - 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 { + let text = self.text(); let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); let foreground = THEME .settings @@ -161,8 +133,8 @@ impl Show for RawNode { let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { let root = match lang.as_deref() { - Some("typc") => syntax::parse_code(&self.text), - _ => syntax::parse(&self.text), + Some("typc") => syntax::parse_code(&text), + _ => syntax::parse(&text), }; let mut seq = vec![]; @@ -172,7 +144,7 @@ impl Show for RawNode { vec![], &highlighter, &mut |node, style| { - seq.push(styled(&self.text[node.range()], foreground, style)); + seq.push(styled(&text[node.range()], foreground, style)); }, ); @@ -182,9 +154,9 @@ impl Show for RawNode { { let mut seq = vec![]; let mut highlighter = syntect::easy::HighlightLines::new(syntax, &THEME); - for (i, line) in self.text.lines().enumerate() { + for (i, line) in text.lines().enumerate() { if i != 0 { - seq.push(LinebreakNode { justify: false }.pack()); + seq.push(LinebreakNode::new().pack()); } for (style, piece) in @@ -196,16 +168,11 @@ impl Show for RawNode { Content::sequence(seq) } else { - TextNode::packed(self.text.clone()) + TextNode::packed(text) }; - if self.block { - realized = BlockNode { - body: realized, - width: Smart::Auto, - height: Smart::Auto, - } - .pack(); + if self.block() { + realized = BlockNode::new().with_body(realized).pack(); } Ok(realized) diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index feb9b24b5..709cce258 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -154,7 +154,7 @@ impl<'a> ShapedText<'a> { for family in families(self.styles) { if let Some(font) = world .book() - .select(family, self.variant) + .select(family.as_str(), self.variant) .and_then(|id| world.font(id)) { expand(&font); @@ -209,7 +209,7 @@ impl<'a> ShapedText<'a> { let world = vt.world(); let font = world .book() - .select(family, self.variant) + .select(family.as_str(), self.variant) .and_then(|id| world.font(id))?; let ttf = font.ttf(); let glyph_id = ttf.glyph_index('-')?; @@ -351,7 +351,7 @@ fn shape_segment<'a>( ctx: &mut ShapingContext, base: usize, text: &str, - mut families: impl Iterator + Clone, + mut families: impl Iterator + 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 + Clone { +pub fn families(styles: StyleChain) -> impl Iterator + Clone { const FALLBACKS: &[&str] = &[ "linux libertine", "twitter color emoji", @@ -562,9 +562,8 @@ pub fn families(styles: StyleChain) -> impl Iterator + Clone { styles .get(TextNode::FAMILY) .0 - .iter() - .map(|family| family.as_str()) - .chain(tail.iter().copied()) + .into_iter() + .chain(tail.iter().copied().map(FontFamily::new)) } /// Collect the tags of the OpenType features to apply. diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs index d68095918..105953b62 100644 --- a/library/src/text/shift.rs +++ b/library/src/text/shift.rs @@ -3,7 +3,6 @@ use typst::model::SequenceNode; use super::{variant, SpaceNode, TextNode, TextSize}; use crate::prelude::*; -/// # Subscript /// Set text in subscript. /// /// The text is rendered smaller and its baseline is lowered. @@ -13,19 +12,15 @@ use crate::prelude::*; /// Revenue#sub[yearly] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The text to display in subscript. -/// -/// ## Category -/// text -#[func] -#[capable(Show)] -#[derive(Debug, Hash)] -pub struct SubNode(pub Content); +/// 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 { - 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 { + 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 { - 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 { + 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 { if content.is::() { Some(' '.into()) - } else if let Some(text) = content.to::() { - convert_script(&text.0, sub) + } else if let Some(node) = content.to::() { + convert_script(&node.text(), sub) } else if let Some(seq) = content.to::() { let mut full = EcoString::new(); - for item in seq.0.iter() { - match search_text(item, sub) { + for item in seq.children() { + match search_text(&item, sub) { Some(text) => full.push_str(&text), None => return None, } diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs index 5e3c7f83e..fa5b70ad3 100644 --- a/library/src/visualize/image.rs +++ b/library/src/visualize/image.rs @@ -1,10 +1,10 @@ use std::ffi::OsStr; +use std::path::Path; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use crate::prelude::*; -/// # Image /// A raster or vector graphic. /// /// Supported formats are PNG, JPEG, GIF and SVG. @@ -18,62 +18,52 @@ use crate::prelude::*; /// ] /// ``` /// -/// ## Parameters -/// - path: `EcoString` (positional, required) -/// Path to an image file. -/// -/// - width: `Rel` (named) -/// The width of the image. -/// -/// - height: `Rel` (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>, + + /// The height of the image. + #[named] + #[default] pub height: Smart>, + + /// 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 { let Spanned { v: path, span } = args.expect::>("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::>>("width")?.unwrap_or_default(); + let height = args.named::>>("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 { - 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, full: &str) -> StrResult { + let full = Path::new(full); + let buffer = world.file(full)?; + let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); + let format = match ext.to_lowercase().as_str() { + "png" => ImageFormat::Raster(RasterFormat::Png), + "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), + "gif" => ImageFormat::Raster(RasterFormat::Gif), + "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), + _ => return Err("unknown image format".into()), + }; + Image::new(buffer, format) +} diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs index 553e06c88..0e0a272f0 100644 --- a/library/src/visualize/line.rs +++ b/library/src/visualize/line.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Line /// A line from one point to another. /// /// ## Example @@ -26,20 +25,20 @@ use crate::prelude::*; /// The angle at which the line points away from the origin. Mutually /// exclusive with `end`. /// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Line +/// Category: visualize +#[node(Construct, Layout)] pub struct LineNode { /// Where the line starts. + #[named] + #[default] pub start: Axes>, - /// The offset from `start` where the line ends. - pub delta: Axes>, -} -#[node] -impl LineNode { + /// The offset from `start` where the line ends. + #[named] + #[default] + pub delta: Axes>, + /// 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 { let start = args.named("start")?.unwrap_or_default(); - let delta = match args.named::>>("end")? { Some(end) => end.zip(start).map(|(to, from)| to - from), None => { @@ -71,8 +74,7 @@ impl LineNode { Axes::new(x, y) } }; - - Ok(Self { start, delta }.pack()) + Ok(Self::new().with_start(start).with_delta(delta).pack()) } } @@ -86,13 +88,13 @@ impl Layout for LineNode { let stroke = styles.get(Self::STROKE).unwrap_or_default(); let origin = self - .start + .start() .resolve(styles) .zip(regions.base()) .map(|(l, b)| l.relative_to(b)); let delta = self - .delta + .delta() .resolve(styles) .zip(regions.base()) .map(|(l, b)| l.relative_to(b)); diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs index e5259d917..ef81a871f 100644 --- a/library/src/visualize/shape.rs +++ b/library/src/visualize/shape.rs @@ -2,7 +2,6 @@ use std::f64::consts::SQRT_2; use crate::prelude::*; -/// # Rectangle /// A rectangle with optional content. /// /// ## Example @@ -17,32 +16,28 @@ use crate::prelude::*; /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// The content to place into the rectangle. -/// -/// When this is omitted, the rectangle takes on a default size of at most -/// `{45pt}` by `{30pt}`. -/// -/// - width: `Rel` (named) -/// The rectangle's width, relative to its parent container. -/// -/// - height: `Rel` (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, - pub width: Smart>, - pub height: Smart>, -} -#[node] -impl RectNode { + /// The rectangle's width, relative to its parent container. + #[named] + #[default] + pub width: Smart>, + + /// The rectangle's height, relative to its parent container. + #[named] + #[default] + pub height: Smart>, + /// 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 = None; + #[settable] + #[default] + pub fill: Option, /// 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>>> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Smart>>>, /// 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>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners>>, /// 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>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[resolve] + #[fold] + #[default(Sides::splat(Abs::pt(5.0).into()))] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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>>, } 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` (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` (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, - pub width: Smart>, - pub height: Smart>, -} -#[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>, + + /// 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>, + /// How to fill the square. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option = None; + #[settable] + #[default] + pub fill: Option, /// How to stroke the square. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart>>> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Smart>>>, /// 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>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners>>, /// 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>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[resolve] + #[fold] + #[default(Sides::splat(Abs::pt(5.0).into()))] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides>>, +} +impl Construct for SquareNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let size = args.named::>("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::()?; + 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` (named) -/// The ellipse's width, relative to its parent container. -/// -/// - height: `Rel` (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, - pub width: Smart>, - pub height: Smart>, -} -#[node] -impl EllipseNode { + /// The ellipse's width, relative to its parent container. + #[named] + #[default] + pub width: Smart>, + + /// The ellipse's height, relative to its parent container. + #[named] + #[default] + pub height: Smart>, + /// How to fill the ellipse. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option = None; + #[settable] + #[default] + pub fill: Option, /// How to stroke the ellipse. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Smart>, /// 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>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[resolve] + #[fold] + #[default(Sides::splat(Abs::pt(5.0).into()))] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - 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>>, } 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, - pub width: Smart>, - pub height: Smart>, -} -#[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>, + + /// 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>, + /// How to fill the circle. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option = None; + #[settable] + #[default] + pub fill: Option, /// How to stroke the circle. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default(Smart::Auto)] + pub stroke: Smart>, /// 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>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[resolve] + #[fold] + #[default(Sides::splat(Abs::pt(5.0).into()))] + pub inset: Sides>>, /// 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>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides>>, +} +impl Construct for CircleNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let size = args .named::>("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::()?; + Ok(Self::new() + .with_body(body) + .with_width(width) + .with_height(height) + .pack()) } } @@ -488,8 +531,8 @@ impl Layout for CircleNode { styles, regions, ShapeKind::Circle, - &self.body, - Axes::new(self.width, self.height), + &self.body(), + Axes::new(self.width(), self.height()), styles.get(Self::FILL), styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::INSET), diff --git a/macros/src/capable.rs b/macros/src/capable.rs deleted file mode 100644 index dcfdfc829..000000000 --- a/macros/src/capable.rs +++ /dev/null @@ -1,48 +0,0 @@ -use syn::parse::Parser; -use syn::punctuated::Punctuated; -use syn::Token; - -use super::*; - -/// Expand the `#[capability]` macro. -pub fn capability(item: syn::ItemTrait) -> Result { - 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 { - 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::::parse_terminated - .parse2(attr)? - .into_iter() - .map(|capability| { - quote! { - if id == ::std::any::TypeId::of::() { - return Some(unsafe { - ::typst::util::fat::vtable(self as &dyn #capability) - }); - } - } - }); - - Ok(quote! { - #item - - unsafe impl #params ::typst::model::Capable for #ident #args #clause { - fn vtable(&self, id: ::std::any::TypeId) -> ::std::option::Option<*const ()> { - #(#checks)* - None - } - } - }) -} diff --git a/macros/src/castable.rs b/macros/src/castable.rs index c39df90af..c0d0c1ad6 100644 --- a/macros/src/castable.rs +++ b/macros/src/castable.rs @@ -1,11 +1,7 @@ -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::Token; - use super::*; -/// Expand the `castable!` macro. -pub fn castable(stream: TokenStream) -> Result { +/// Expand the `cast_from_value!` macro. +pub fn cast_from_value(stream: TokenStream) -> Result { let castable: Castable = syn::parse2(stream)?; let ty = &castable.ty; @@ -41,6 +37,77 @@ pub fn castable(stream: TokenStream) -> Result { }) } +/// Expand the `cast_to_value!` macro. +pub fn cast_to_value(stream: TokenStream) -> Result { + 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, + casts: Punctuated, +} + +struct Cast { + attrs: Vec, + pattern: Pattern, + expr: syn::Expr, +} + +enum Pattern { + Str(syn::LitStr), + Ty(syn::Pat, syn::Type), +} + +impl Parse for Castable { + fn parse(input: ParseStream) -> Result { + 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 { + 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 { + 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, - casts: Punctuated, -} - -impl Parse for Castable { - fn parse(input: ParseStream) -> Result { - 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, - pattern: Pattern, - expr: syn::Expr, -} - -impl Parse for Cast { - fn parse(input: ParseStream) -> Result { - 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 { - if input.peek(syn::LitStr) { - Ok(Pattern::Str(input.parse()?)) - } else { - let pat = input.parse()?; - let _: syn::Token![:] = input.parse()?; - let ty = input.parse()?; - Ok(Pattern::Ty(pat, ty)) - } - } -} diff --git a/macros/src/func.rs b/macros/src/func.rs index f65c135ed..01c3ca0e5 100644 --- a/macros/src/func.rs +++ b/macros/src/func.rs @@ -1,30 +1,22 @@ -use unscanny::Scanner; - use super::*; /// Expand the `#[func]` macro. pub fn func(item: syn::Item) -> Result { - 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 { } /// Extract a section. -pub fn section(docs: &mut String, title: &str, level: usize) -> Option { +fn section(docs: &mut String, title: &str, level: usize) -> Option { let hashtags = "#".repeat(level); let needle = format!("\n{hashtags} {title}\n"); let start = docs.find(&needle)?; diff --git a/macros/src/lib.rs b/macros/src/lib.rs index cd0f99887..c1a8b2ae1 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -2,32 +2,23 @@ extern crate proc_macro; -/// Return an error at the given item. -macro_rules! bail { - (callsite, $fmt:literal $($tts:tt)*) => { - return Err(syn::Error::new( - proc_macro2::Span::call_site(), - format!(concat!("typst: ", $fmt) $($tts)*) - )) - }; - ($item:expr, $fmt:literal $($tts:tt)*) => { - return Err(syn::Error::new_spanned( - &$item, - format!(concat!("typst: ", $fmt) $($tts)*) - )) - }; -} - -mod capable; +#[macro_use] +mod util; mod castable; mod func; mod node; mod symbols; use proc_macro::TokenStream as BoundaryStream; -use proc_macro2::{TokenStream, TokenTree}; -use quote::{quote, quote_spanned}; -use syn::{parse_quote, Ident, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, Ident, Result, Token}; +use unscanny::Scanner; + +use self::util::*; /// Implement `FuncType` for a type or function. #[proc_macro_attribute] @@ -38,33 +29,25 @@ pub fn func(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { /// Implement `Node` for a struct. #[proc_macro_attribute] -pub fn node(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as syn::ItemImpl); - node::node(item).unwrap_or_else(|err| err.to_compile_error()).into() -} - -/// Implement `Capability` for a trait. -#[proc_macro_attribute] -pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as syn::ItemTrait); - capable::capability(item) - .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 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::>() - .join("\n") -} diff --git a/macros/src/node.rs b/macros/src/node.rs index 0d59a4029..8a6660ca1 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -1,309 +1,370 @@ -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::Token; - use super::*; /// Expand the `#[node]` macro. -pub fn node(body: syn::ItemImpl) -> Result { - let node = prepare(body)?; - create(&node) +pub fn node(stream: TokenStream, body: syn::ItemStruct) -> Result { + let node = prepare(stream, &body)?; + Ok(create(&node)) } -/// Details about a node. struct Node { - body: syn::ItemImpl, - params: Punctuated, - self_ty: syn::Type, - self_name: String, - self_args: Punctuated, - properties: Vec, - construct: Option, - set: Option, - field: Option, -} - -/// A style property. -struct Property { attrs: Vec, vis: syn::Visibility, - name: Ident, - value_ty: syn::Type, - output_ty: syn::Type, - default: syn::Expr, - skip: bool, - referenced: bool, - shorthand: Option, - resolve: bool, - fold: bool, + ident: Ident, + name: String, + capable: Vec, + set: Option, + fields: Vec, +} + +struct Field { + attrs: Vec, + vis: syn::Visibility, + ident: Ident, + with_ident: Ident, + name: String, + + positional: bool, + required: bool, + variadic: bool, + + named: bool, + shorthand: Option, + + settable: bool, + fold: bool, + resolve: bool, + skip: bool, + + ty: syn::Type, + default: Option, } -/// 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 { - // 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 + Clone { + self.fields.iter().filter(|field| !field.settable) } + fn settable(&self) -> impl Iterator + Clone { + self.fields.iter().filter(|field| field.settable) + } +} + +/// Preprocess the node's definition. +fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { + 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::::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 { - 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::()?, - 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 { - 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 = vec![]; - let mut items: Vec = 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::() +/// 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::()) + #(#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::()) + #(#builders)*)) } } + } +} + +/// Create let bindings for shorthands in the constructor. +fn create_construct_shorthands(node: &Node) -> impl Iterator + '_ { + 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(pub ::std::marker::PhantomData); + 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, + ) -> 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::() { + 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, - ) -> Self::Output<'a> { - #value - } - } -} - -/// Create the assertion if the property's value must be copyable. -fn create_property_copy_assertion(property: &Property) -> Option { - 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() {} - must_be_copy_fold_resolve_or_referenced::<#value_ty>(); - }; - } - }) -} diff --git a/macros/src/symbols.rs b/macros/src/symbols.rs index efa4834d3..cdb7f5d74 100644 --- a/macros/src/symbols.rs +++ b/macros/src/symbols.rs @@ -1,30 +1,44 @@ -use syn::ext::IdentExt; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::Token; - use super::*; /// Expand the `symbols!` macro. pub fn symbols(stream: TokenStream) -> Result { - let list: List = syn::parse2(stream)?; - let pairs = list.0.iter().map(Symbol::expand); + let list: Punctuated = + 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); - -impl Parse for List { - fn parse(input: ParseStream) -> Result { - Punctuated::parse_terminated(input).map(Self) - } -} - struct Symbol { name: syn::Ident, kind: Kind, } +enum Kind { + Single(syn::LitChar), + Multiple(Punctuated), +} + +struct Variant { + name: String, + c: syn::LitChar, +} + impl Parse for Symbol { fn parse(input: ParseStream) -> Result { 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), -} - impl Parse for Kind { fn parse(input: ParseStream) -> Result { 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 { let mut name = String::new(); @@ -94,11 +76,3 @@ impl Parse for Variant { Ok(Self { name, c }) } } - -impl Variant { - fn expand(&self) -> TokenStream { - let name = &self.name; - let c = &self.c; - quote! { (#name, #c) } - } -} diff --git a/macros/src/util.rs b/macros/src/util.rs new file mode 100644 index 000000000..c8c56a054 --- /dev/null +++ b/macros/src/util.rs @@ -0,0 +1,88 @@ +use super::*; + +/// Return an error at the given item. +macro_rules! bail { + (callsite, $fmt:literal $($tts:tt)*) => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!(concat!("typst: ", $fmt) $($tts)*) + )) + }; + ($item:expr, $fmt:literal $($tts:tt)*) => { + return Err(syn::Error::new_spanned( + &$item, + format!(concat!("typst: ", $fmt) $($tts)*) + )) + }; +} + +/// Whether an attribute list has a specified attribute. +pub fn has_attr(attrs: &mut Vec, target: &str) -> bool { + take_attr(attrs, target).is_some() +} + +/// Whether an attribute list has a specified attribute. +pub fn parse_attr( + attrs: &mut Vec, + target: &str, +) -> Result>> { + 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, + target: &str, +) -> Option { + 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::>() + .join("\n") +} + +/// Extract documentation comments from an attribute list. +pub fn documentation(attrs: &[syn::Attribute]) -> String { + let mut doc = String::new(); + + // Parse doc comments. + for attr in attrs { + if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() { + if meta.path.is_ident("doc") { + if let syn::Lit::Str(string) = &meta.lit { + let full = string.value(); + let line = full.strip_prefix(' ').unwrap_or(&full); + doc.push_str(line); + doc.push('\n'); + } + } + } + } + + doc.trim().into() +} diff --git a/src/doc.rs b/src/doc.rs index cba3ca99c..9ac2f68d1 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -7,14 +7,14 @@ use std::sync::Arc; use ecow::EcoString; -use crate::eval::{dict, Dict, Value}; +use crate::eval::{cast_from_value, cast_to_value, dict, Dict, Value}; use crate::font::Font; use crate::geom::{ - self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric, - Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, + self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, + Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, }; use crate::image::Image; -use crate::model::{capable, node, Content, Fold, StableId, StyleChain}; +use crate::model::{node, Content, Fold, StableId, StyleChain}; /// A finished document with metadata and page frames. #[derive(Debug, Default, Clone, Hash)] @@ -274,7 +274,7 @@ impl Frame { if self.is_empty() { return; } - for meta in styles.get(Meta::DATA) { + for meta in styles.get(MetaNode::DATA) { if matches!(meta, Meta::Hidden) { self.clear(); break; @@ -283,6 +283,14 @@ impl Frame { } } + /// Add a background fill. + pub fn fill(&mut self, fill: Paint) { + self.prepend( + Point::zero(), + Element::Shape(Geometry::Rect(self.size()).filled(fill)), + ); + } + /// Add a fill and stroke with optional radius and outset to the frame. pub fn fill_and_stroke( &mut self, @@ -533,6 +541,15 @@ impl FromStr for Lang { } } +cast_from_value! { + Lang, + string: EcoString => Self::from_str(&string)?, +} + +cast_to_value! { + v: Lang => v.as_str().into() +} + /// An identifier for a region somewhere in the world. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Region([u8; 2]); @@ -559,8 +576,16 @@ impl FromStr for Region { } } +cast_from_value! { + Region, + string: EcoString => Self::from_str(&string)?, +} + +cast_to_value! { + v: Region => v.as_str().into() +} + /// Meta information that isn't visible or renderable. -#[capable] #[derive(Debug, Clone, Hash)] pub enum Meta { /// An internal or external link. @@ -572,12 +597,16 @@ pub enum Meta { Hidden, } +/// Host for metadata. #[node] -impl Meta { +pub struct MetaNode { /// Metadata that should be attached to all elements affected by this style /// property. - #[property(fold, skip)] - pub const DATA: Vec = vec![]; + #[settable] + #[fold] + #[skip] + #[default] + pub data: Vec, } impl Fold for Vec { @@ -589,6 +618,16 @@ impl Fold for Vec { } } +cast_from_value! { + Meta: "meta", +} + +impl PartialEq for Meta { + fn eq(&self, other: &Self) -> bool { + crate::util::hash128(self) == crate::util::hash128(other) + } +} + /// A link destination. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { @@ -598,6 +637,19 @@ pub enum Destination { Url(EcoString), } +cast_from_value! { + Destination, + loc: Location => Self::Internal(loc), + string: EcoString => Self::Url(string), +} + +cast_to_value! { + v: Destination => match v { + Destination::Internal(loc) => loc.into(), + Destination::Url(url) => url.into(), + } +} + /// A physical location in a document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Location { @@ -607,53 +659,21 @@ pub struct Location { pub pos: Point, } -impl Location { - /// Encode into a user-facing dictionary. - pub fn encode(&self) -> Dict { - dict! { - "page" => Value::Int(self.page.get() as i64), - "x" => Value::Length(self.pos.x.into()), - "y" => Value::Length(self.pos.y.into()), - } - } +cast_from_value! { + Location, + mut dict: Dict => { + let page = dict.take("page")?.cast()?; + let x: Length = dict.take("x")?.cast()?; + let y: Length = dict.take("y")?.cast()?; + dict.finish(&["page", "x", "y"])?; + Self { page, pos: Point::new(x.abs, y.abs) } + }, } -/// Standard semantic roles. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Role { - /// A paragraph. - Paragraph, - /// A heading of the given level and whether it should be part of the - /// outline. - Heading { level: NonZeroUsize, outlined: bool }, - /// A generic block-level subdivision. - GenericBlock, - /// A generic inline subdivision. - GenericInline, - /// A list and whether it is ordered. - List { ordered: bool }, - /// A list item. Must have a list parent. - ListItem, - /// The label of a list item. Must have a list item parent. - ListLabel, - /// The body of a list item. Must have a list item parent. - ListItemBody, - /// A mathematical formula. - Formula, - /// A table. - Table, - /// A table row. Must have a table parent. - TableRow, - /// A table cell. Must have a table row parent. - TableCell, - /// A code fragment. - Code, - /// A page header. - Header, - /// A page footer. - Footer, - /// A page background. - Background, - /// A page foreground. - Foreground, +cast_to_value! { + v: Location => Value::Dict(dict! { + "page" => Value::Int(v.page.get() as i64), + "x" => Value::Length(v.pos.x.into()), + "y" => Value::Length(v.pos.y.into()), + }) } diff --git a/src/eval/cast.rs b/src/eval/cast.rs index 77521f7f5..840ceb058 100644 --- a/src/eval/cast.rs +++ b/src/eval/cast.rs @@ -1,18 +1,12 @@ +pub use typst_macros::{cast_from_value, cast_to_value}; + use std::num::NonZeroUsize; use std::ops::Add; -use std::str::FromStr; use ecow::EcoString; -use super::{castable, Array, Dict, Func, Regex, Str, Value}; +use super::{Array, Str, Value}; use crate::diag::StrResult; -use crate::doc::{Destination, Lang, Location, Region}; -use crate::font::{FontStretch, FontStyle, FontWeight}; -use crate::geom::{ - Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio, - Rel, Sides, Smart, -}; -use crate::model::{Content, Label, Selector, Transform}; use crate::syntax::Spanned; /// Cast from a value to a specific type. @@ -32,6 +26,191 @@ pub trait Cast: Sized { } } +impl Cast for Value { + fn is(_: &Value) -> bool { + true + } + + fn cast(value: Value) -> StrResult { + Ok(value) + } + + fn describe() -> CastInfo { + CastInfo::Any + } +} + +impl Cast> for T { + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + T::cast(value.v) + } + + fn describe() -> CastInfo { + T::describe() + } +} + +impl Cast> for Spanned { + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + 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 Cast for Option { + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::None => Ok(None), + v if T::is(&v) => Ok(Some(T::cast(v)?)), + _ => ::error(value), + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("none") + } +} + +impl> From> for Value { + fn from(v: Option) -> Self { + match v { + Some(v) => v.into(), + None => Value::None, + } + } +} + +impl Cast for Vec { + fn is(value: &Value) -> bool { + Array::is(value) + } + + fn cast(value: Value) -> StrResult { + value.cast::()?.into_iter().map(Value::cast).collect() + } + + fn describe() -> CastInfo { + ::describe() + } +} + +impl> From> for Value { + fn from(v: Vec) -> 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 { - Ok(value) + ::error(value) } fn describe() -> CastInfo { - CastInfo::Any - } -} - -impl Cast> for T { - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - T::cast(value.v) - } - - fn describe() -> CastInfo { - T::describe() - } -} - -impl Cast> for Spanned { - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - 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: "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>, - align: GenAlign => { - let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(align)); - aligns - }, - aligns: Axes => aligns.map(Some), -} - -castable! { - Axes>, - 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 { - match value { - Value::None => Ok(Self), - _ => ::error(value), - } - } - - fn describe() -> CastInfo { - CastInfo::Type("none") - } -} - -impl Cast for Option { - fn is(value: &Value) -> bool { - matches!(value, Value::None) || T::is(value) - } - - fn cast(value: Value) -> StrResult { - match value { - Value::None => Ok(None), - v if T::is(&v) => Ok(Some(T::cast(v)?)), - _ => ::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 { - match value { - Value::Auto => Ok(Self), - _ => ::error(value), - } - } - - fn describe() -> CastInfo { - CastInfo::Type("auto") - } -} - -impl Cast for Smart { - fn is(value: &Value) -> bool { - matches!(value, Value::Auto) || T::is(value) - } - - fn cast(value: Value) -> StrResult { - match value { - Value::Auto => Ok(Self::Auto), - v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), - _ => ::error(value), - } - } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("auto") - } -} - -impl Cast for Sides> -where - T: Cast + Copy, -{ - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) - } - - fn cast(mut value: Value) -> StrResult { - 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 { - ::error(value) - } - } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } -} - -impl Cast for Corners> -where - T: Cast + Copy, -{ - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) - } - - fn cast(mut value: Value) -> StrResult { - 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 { - ::error(value) - } - } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") + CastInfo::Union(vec![]) } } diff --git a/src/eval/func.rs b/src/eval/func.rs index e5280932a..8243b4f60 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -1,3 +1,5 @@ +pub use typst_macros::func; + use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; diff --git a/src/eval/library.rs b/src/eval/library.rs index adfcc6e7c..75787348c 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -44,7 +44,7 @@ pub struct LangItems { /// The id of the text node. pub text_id: NodeId, /// Get the string if this is a text node. - pub text_str: fn(&Content) -> Option<&str>, + pub text_str: fn(&Content) -> Option, /// A smart quote: `'` or `"`. pub smart_quote: fn(double: bool) -> Content, /// A paragraph break. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2cf6f4d19..8180f11de 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -20,8 +20,6 @@ mod ops; mod scope; mod symbol; -pub use typst_macros::{castable, func}; - pub use self::args::*; pub use self::array::*; pub use self::cast::*; diff --git a/src/eval/str.rs b/src/eval/str.rs index 63ea5dc8b..0d5d71b96 100644 --- a/src/eval/str.rs +++ b/src/eval/str.rs @@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Deref}; use ecow::EcoString; use unicode_segmentation::UnicodeSegmentation; -use super::{castable, dict, Array, Dict, Value}; +use super::{cast_from_value, dict, Array, Dict, Value}; use crate::diag::StrResult; use crate::geom::GenAlign; @@ -479,6 +479,10 @@ impl Hash for Regex { } } +cast_from_value! { + Regex: "regular expression", +} + /// A pattern which can be searched for in a string. #[derive(Debug, Clone)] pub enum StrPattern { @@ -488,7 +492,7 @@ pub enum StrPattern { Regex(Regex), } -castable! { +cast_from_value! { StrPattern, text: Str => Self::Str(text), regex: Regex => Self::Regex(regex), @@ -504,7 +508,7 @@ pub enum StrSide { End, } -castable! { +cast_from_value! { StrSide, align: GenAlign => match align { GenAlign::Start => Self::Start, diff --git a/src/eval/value.rs b/src/eval/value.rs index 5e06da76d..9b9bc3149 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -4,15 +4,15 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use siphasher::sip128::{Hasher128, SipHasher}; use super::{ - format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Module, - Str, Symbol, + cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, + Label, Module, Str, Symbol, }; use crate::diag::StrResult; -use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; +use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel}; use crate::syntax::{ast, Span}; /// A computational value. @@ -122,6 +122,7 @@ impl Value { Self::Dict(dict) => dict.at(&field).cloned(), Self::Content(content) => content .field(&field) + .cloned() .ok_or_else(|| eco_format!("unknown field `{field}`")), Self::Module(module) => module.get(&field).cloned(), v => Err(eco_format!("cannot access fields on type {}", v.type_name())), @@ -241,60 +242,6 @@ impl Hash for Value { } } -impl From for Value { - fn from(v: i32) -> Self { - Self::Int(v as i64) - } -} - -impl From for Value { - fn from(v: usize) -> Self { - Self::Int(v as i64) - } -} - -impl From for Value { - fn from(v: Abs) -> Self { - Self::Length(v.into()) - } -} - -impl From for Value { - fn from(v: Em) -> Self { - Self::Length(v.into()) - } -} - -impl From 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 for Value { - fn from(v: EcoString) -> Self { - Self::Str(v.into()) - } -} - -impl From for Value { - fn from(v: String) -> Self { - Self::Str(v.into()) - } -} - -impl From for Value { - fn from(v: Dynamic) -> Self { - Self::Dyn(v) - } -} - /// A dynamic value. #[derive(Clone, Hash)] pub struct Dynamic(Arc); @@ -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, exp: &str) { diff --git a/src/font/mod.rs b/src/font/mod.rs index bedc107d1..94ec170e9 100644 --- a/src/font/mod.rs +++ b/src/font/mod.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use ttf_parser::GlyphId; +use crate::eval::{cast_from_value, cast_to_value, Value}; use crate::geom::Em; use crate::util::Buffer; @@ -249,3 +250,27 @@ pub enum VerticalFontMetric { /// present and falls back to the descender from the `hhea` table otherwise. Descender, } + +cast_from_value! { + VerticalFontMetric, + /// The font's ascender, which typically exceeds the height of all glyphs. + "ascender" => Self::Ascender, + /// The approximate height of uppercase letters. + "cap-height" => Self::CapHeight, + /// The approximate height of non-ascending lowercase letters. + "x-height" => Self::XHeight, + /// The baseline on which the letters rest. + "baseline" => Self::Baseline, + /// The font's ascender, which typically exceeds the depth of all glyphs. + "descender" => Self::Descender, +} + +cast_to_value! { + v: VerticalFontMetric => Value::from(match v { + VerticalFontMetric::Ascender => "ascender", + VerticalFontMetric::CapHeight => "cap-height", + VerticalFontMetric::XHeight => "x-height", + VerticalFontMetric::Baseline => "baseline" , + VerticalFontMetric::Descender => "descender", + }) +} diff --git a/src/font/variant.rs b/src/font/variant.rs index aa9ff1418..4eda80ad7 100644 --- a/src/font/variant.rs +++ b/src/font/variant.rs @@ -2,6 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Serialize}; +use crate::eval::{cast_from_value, cast_to_value, Value}; +use crate::geom::Ratio; + /// Properties that distinguish a font from other fonts in the same family. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] @@ -59,6 +62,24 @@ impl Default for FontStyle { } } +cast_from_value! { + FontStyle, + /// The default, typically upright style. + "normal" => Self::Normal, + /// A cursive style with custom letterform. + "italic" => Self::Italic, + /// Just a slanted version of the normal style. + "oblique" => Self::Oblique, +} + +cast_to_value! { + v: FontStyle => Value::from(match v { + FontStyle::Normal => "normal", + FontStyle::Italic => "italic", + FontStyle::Oblique => "oblique", + }) +} + /// The weight of a font. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] @@ -127,6 +148,44 @@ impl Debug for FontWeight { } } +cast_from_value! { + FontWeight, + v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), + /// Thin weight (100). + "thin" => Self::THIN, + /// Extra light weight (200). + "extralight" => Self::EXTRALIGHT, + /// Light weight (300). + "light" => Self::LIGHT, + /// Regular weight (400). + "regular" => Self::REGULAR, + /// Medium weight (500). + "medium" => Self::MEDIUM, + /// Semibold weight (600). + "semibold" => Self::SEMIBOLD, + /// Bold weight (700). + "bold" => Self::BOLD, + /// Extrabold weight (800). + "extrabold" => Self::EXTRABOLD, + /// Black weight (900). + "black" => Self::BLACK, +} + +cast_to_value! { + v: FontWeight => Value::from(match v { + FontWeight::THIN => "thin", + FontWeight::EXTRALIGHT => "extralight", + FontWeight::LIGHT => "light", + FontWeight::REGULAR => "regular", + FontWeight::MEDIUM => "medium", + FontWeight::SEMIBOLD => "semibold", + FontWeight::BOLD => "bold", + FontWeight::EXTRABOLD => "extrabold", + FontWeight::BLACK => "black", + _ => return v.to_number().into(), + }) +} + /// The width of a font. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] @@ -163,8 +222,8 @@ impl FontStretch { /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if /// necessary. - pub fn from_ratio(ratio: f32) -> Self { - Self((ratio.max(0.5).min(2.0) * 1000.0) as u16) + pub fn from_ratio(ratio: Ratio) -> Self { + Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16) } /// Create a font stretch from an OpenType-style number between 1 and 9, @@ -184,12 +243,12 @@ impl FontStretch { } /// The ratio between 0.5 and 2.0 corresponding to this stretch. - pub fn to_ratio(self) -> f32 { - self.0 as f32 / 1000.0 + pub fn to_ratio(self) -> Ratio { + Ratio::new(self.0 as f64 / 1000.0) } /// The absolute ratio distance between this and another font stretch. - pub fn distance(self, other: Self) -> f32 { + pub fn distance(self, other: Self) -> Ratio { (self.to_ratio() - other.to_ratio()).abs() } } @@ -202,10 +261,19 @@ impl Default for FontStretch { impl Debug for FontStretch { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}%", 100.0 * self.to_ratio()) + self.to_ratio().fmt(f) } } +cast_from_value! { + FontStretch, + v: Ratio => Self::from_ratio(v), +} + +cast_to_value! { + v: FontStretch => v.to_ratio().into() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/geom/abs.rs b/src/geom/abs.rs index 4429e46dd..34c3d0107 100644 --- a/src/geom/abs.rs +++ b/src/geom/abs.rs @@ -214,6 +214,10 @@ impl<'a> Sum<&'a Self> for Abs { } } +cast_to_value! { + v: Abs => Value::Length(v.into()) +} + /// Different units of absolute measurement. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum AbsUnit { diff --git a/src/geom/align.rs b/src/geom/align.rs index 1e9bde522..b14e6775b 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -115,3 +115,51 @@ impl Debug for GenAlign { } } } + +cast_from_value! { + GenAlign: "alignment", +} + +cast_from_value! { + Axes: "2d alignment", +} + +cast_from_value! { + Axes>, + align: GenAlign => { + let mut aligns = Axes::default(); + aligns.set(align.axis(), Some(align)); + aligns + }, + aligns: Axes => aligns.map(Some), +} + +cast_to_value! { + v: Axes> => match (v.x, v.y) { + (Some(x), Some(y)) => Axes::new(x, y).into(), + (Some(x), None) => x.into(), + (None, Some(y)) => y.into(), + (None, None) => Value::None, + } +} + +impl Resolve for GenAlign { + type Output = Align; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let dir = item!(dir)(styles); + match self { + Self::Start => dir.start().into(), + Self::End => dir.end().into(), + Self::Specific(align) => align, + } + } +} + +impl Fold for GenAlign { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} diff --git a/src/geom/axes.rs b/src/geom/axes.rs index 48f8c0e88..92bd03886 100644 --- a/src/geom/axes.rs +++ b/src/geom/axes.rs @@ -2,6 +2,7 @@ use std::any::Any; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use super::*; +use crate::eval::Array; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] @@ -272,3 +273,37 @@ impl BitAndAssign for Axes { self.y &= rhs.y; } } + +cast_from_value! { + Axes>, + 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> => Value::Array(array![v.x, v.y]) +} + +impl Resolve for Axes { + type Output = Axes; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Axes> { + type Output = Axes; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} diff --git a/src/geom/corners.rs b/src/geom/corners.rs index 386acbfb2..844d3047a 100644 --- a/src/geom/corners.rs +++ b/src/geom/corners.rs @@ -107,3 +107,100 @@ pub enum Corner { /// The bottom left corner. BottomLeft, } + +impl Cast for Corners> +where + T: Cast + Copy, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } + + fn cast(mut value: Value) -> StrResult { + 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 { + ::error(value) + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } +} + +impl Resolve for Corners { + type Output = Corners; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Corners> { + type Output = Corners; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} + +impl From>> for Value +where + T: PartialEq + Into, +{ + fn from(corners: Corners>) -> Self { + if corners.is_uniform() { + if let Some(value) = corners.top_left { + return value.into(); + } + } + + let mut dict = Dict::new(); + if let Some(top_left) = corners.top_left { + dict.insert("top-left".into(), top_left.into()); + } + if let Some(top_right) = corners.top_right { + dict.insert("top-right".into(), top_right.into()); + } + if let Some(bottom_right) = corners.bottom_right { + dict.insert("bottom-right".into(), bottom_right.into()); + } + if let Some(bottom_left) = corners.bottom_left { + dict.insert("bottom-left".into(), bottom_left.into()); + } + + Value::Dict(dict) + } +} diff --git a/src/geom/dir.rs b/src/geom/dir.rs index b2fd6e5a2..bc4d66e16 100644 --- a/src/geom/dir.rs +++ b/src/geom/dir.rs @@ -73,3 +73,7 @@ impl Debug for Dir { }) } } + +cast_from_value! { + Dir: "direction", +} diff --git a/src/geom/em.rs b/src/geom/em.rs index 9f5aff398..2c63c81dd 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -134,3 +134,19 @@ impl Sum for Em { Self(iter.map(|s| s.0).sum()) } } + +cast_to_value! { + v: Em => Value::Length(v.into()) +} + +impl Resolve for Em { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + if self.is_zero() { + Abs::zero() + } else { + self.at(item!(em)(styles)) + } + } +} diff --git a/src/geom/length.rs b/src/geom/length.rs index ae615f14e..f70ea2638 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -124,3 +124,11 @@ assign_impl!(Length += Length); assign_impl!(Length -= Length); assign_impl!(Length *= f64); assign_impl!(Length /= f64); + +impl Resolve for Length { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.abs + self.em.resolve(styles) + } +} diff --git a/src/geom/mod.rs b/src/geom/mod.rs index ebe4436cd..b7daaa1be 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -19,6 +19,7 @@ mod ratio; mod rel; mod rounded; mod scalar; +mod shape; mod sides; mod size; mod smart; @@ -42,6 +43,7 @@ pub use self::ratio::*; pub use self::rel::*; pub use self::rounded::*; pub use self::scalar::*; +pub use self::shape::*; pub use self::sides::*; pub use self::size::*; pub use self::smart::*; @@ -55,6 +57,10 @@ use std::hash::{Hash, Hasher}; use std::iter::Sum; use std::ops::*; +use crate::diag::StrResult; +use crate::eval::{array, cast_from_value, cast_to_value, Cast, CastInfo, Dict, Value}; +use crate::model::{Fold, Resolve, StyleChain}; + /// Generic access to a structure's components. pub trait Get { /// The structure's component type. @@ -72,40 +78,6 @@ pub trait Get { } } -/// 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, - /// The shape's border stroke. - pub stroke: Option, -} - -/// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Geometry { - /// A line to a point (relative to its position). - Line(Point), - /// A rectangle with its origin in the topleft corner. - Rect(Size), - /// A bezier path. - Path(Path), -} - -impl Geometry { - /// Fill the geometry without a stroke. - pub fn filled(self, fill: Paint) -> Shape { - Shape { geometry: self, fill: Some(fill), stroke: None } - } - - /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: Stroke) -> Shape { - Shape { geometry: self, fill: None, stroke: Some(stroke) } - } -} - /// A numeric type. pub trait Numeric: Sized diff --git a/src/geom/paint.rs b/src/geom/paint.rs index b4064438b..c01b21dad 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -9,10 +9,7 @@ pub enum Paint { Solid(Color), } -impl From for Paint -where - T: Into, -{ +impl> From 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 From for Color -where - T: Into, -{ +impl> From for Color { fn from(rgba: T) -> Self { Self::Rgba(rgba.into()) } } +cast_to_value! { + v: RgbaColor => Value::Color(v.into()) +} + /// An 8-bit CMYK color. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct CmykColor { diff --git a/src/geom/rel.rs b/src/geom/rel.rs index a8e75d1c1..7288f3801 100644 --- a/src/geom/rel.rs +++ b/src/geom/rel.rs @@ -199,3 +199,31 @@ impl Add for Rel { self + Rel::from(other) } } + +impl Resolve for Rel +where + T: Resolve + Numeric, + ::Output: Numeric, +{ + type Output = Rel<::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|abs| abs.resolve(styles)) + } +} + +impl Fold for Rel { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} + +impl Fold for Rel { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} diff --git a/src/geom/shape.rs b/src/geom/shape.rs new file mode 100644 index 000000000..5658c21ff --- /dev/null +++ b/src/geom/shape.rs @@ -0,0 +1,35 @@ +use super::*; + +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option, + /// The shape's border stroke. + pub stroke: Option, +} + +/// A shape's geometry. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Geometry { + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. + Rect(Size), + /// A bezier path. + Path(Path), +} + +impl Geometry { + /// Fill the geometry without a stroke. + pub fn filled(self, fill: Paint) -> Shape { + Shape { geometry: self, fill: Some(fill), stroke: None } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: Stroke) -> Shape { + Shape { geometry: self, fill: None, stroke: Some(stroke) } + } +} diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 40327a425..247d9a987 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -177,3 +177,88 @@ impl Side { } } } + +impl Cast for Sides> +where + T: Default + Cast + Copy, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } + + fn cast(mut value: Value) -> StrResult { + 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 { + ::error(value) + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } +} + +impl From>> for Value +where + T: PartialEq + Into, +{ + fn from(sides: Sides>) -> 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 Resolve for Sides { + type Output = Sides; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Sides> { + type Output = Sides; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} diff --git a/src/geom/smart.rs b/src/geom/smart.rs index e115e99da..c977d6519 100644 --- a/src/geom/smart.rs +++ b/src/geom/smart.rs @@ -31,6 +31,18 @@ impl Smart { } } + /// Map the contained custom value with `f` if it contains a custom value, + /// otherwise returns `default`. + pub fn map_or(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) -> Self { match self { @@ -72,3 +84,50 @@ impl Default for Smart { Self::Auto } } + +impl Cast for Smart { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::Auto => Ok(Self::Auto), + v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), + _ => ::error(value), + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("auto") + } +} + +impl Resolve for Smart { + type Output = Smart; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Smart +where + T: Fold, + T::Output: Default, +{ + type Output = Smart; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.map(|inner| inner.fold(outer.unwrap_or_default())) + } +} + +impl> From> for Value { + fn from(v: Smart) -> Self { + match v { + Smart::Custom(v) => v.into(), + Smart::Auto => Value::Auto, + } + } +} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 86191d337..500a4c107 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -58,3 +58,37 @@ impl Debug for PartialStroke { } } } + +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; + + fn resolve(self, styles: StyleChain) -> Self::Output { + PartialStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + } + } +} + +impl Fold for PartialStroke { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + } + } +} diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 06ab53a13..f5eece93d 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -338,6 +338,11 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } } } + Value::Content(content) => { + for (name, value) in content.fields() { + ctx.value_completion(Some(name.clone()), value, false, None); + } + } Value::Dict(dict) => { for (name, value) in dict.iter() { ctx.value_completion(Some(name.clone().into()), value, false, None); diff --git a/src/lib.rs b/src/lib.rs index d73055d14..7cfed897a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,14 +39,13 @@ extern crate self as typst; #[macro_use] pub mod util; #[macro_use] -pub mod geom; -#[macro_use] pub mod diag; #[macro_use] pub mod eval; pub mod doc; pub mod export; pub mod font; +pub mod geom; pub mod ide; pub mod image; pub mod model; diff --git a/src/model/content.rs b/src/model/content.rs index b10a3409d..2af4ae720 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -1,27 +1,24 @@ -use std::any::{Any, TypeId}; -use std::fmt::{self, Debug, Formatter}; +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter, Write}; use std::hash::{Hash, Hasher}; use std::iter::{self, Sum}; use std::ops::{Add, AddAssign}; -use std::sync::Arc; use comemo::Tracked; use ecow::{EcoString, EcoVec}; -use siphasher::sip128::{Hasher128, SipHasher}; -use typst_macros::node; -use super::{capability, capable, Guard, Key, Property, Recipe, Style, StyleMap}; +use super::{node, Guard, Key, Property, Recipe, Style, StyleMap}; use crate::diag::{SourceResult, StrResult}; -use crate::eval::{Args, ParamInfo, Value, Vm}; +use crate::eval::{cast_from_value, Args, Cast, ParamInfo, Value, Vm}; use crate::syntax::Span; -use crate::util::ReadableTypeId; use crate::World; /// Composable representation of styled content. #[derive(Clone, Hash)] pub struct Content { - obj: Arc, + id: NodeId, span: Option, + fields: EcoVec<(EcoString, Value)>, modifiers: EcoVec, } @@ -30,55 +27,43 @@ pub struct Content { enum Modifier { Prepared, Guard(Guard), - Label(Label), - Field(EcoString, Value), } impl Content { + pub fn new() -> 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 { 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::() { - styled.sub.span = Some(span); - } else if let Some(styled) = self.to::() { - self = StyledNode { - sub: styled.sub.clone().spanned(span), - map: styled.map.clone(), - } - .pack(); + if let Some(styled) = self.to::() { + 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, 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::() { - styled.map.apply_one(style); - self - } else if let Some(styled) = self.to::() { - 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::() { + 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::() { - 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 { - 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, + value: impl Into, + ) -> 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, value: impl Into) { + 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(&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(&self) -> bool where - T: Capable + 'static, + T: Node + 'static, { - (*self.obj).as_any().is::() + self.id == NodeId::of::() } /// Cast to `T` if the contained node is of type `T`. pub fn to(&self) -> Option<&T> where - T: Capable + 'static, + T: Node + 'static, { - (*self.obj).as_any().downcast_ref::() + self.is::().then(|| unsafe { std::mem::transmute(self) }) } /// Whether this content has the given capability. pub fn has(&self) -> bool where - C: Capability + ?Sized, + C: ?Sized + 'static, { - self.obj.vtable(TypeId::of::()).is_some() + (self.id.0.vtable)(TypeId::of::()).is_some() } /// Cast to a trait object if this content has the given capability. pub fn with(&self) -> Option<&C> where - C: Capability + ?Sized, + C: ?Sized + 'static, { - let node: &dyn Bounds = &*self.obj; - let vtable = node.vtable(TypeId::of::())?; - let data = node as *const dyn Bounds as *const (); + let vtable = (self.id.0.vtable)(TypeId::of::())?; + 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(&mut self) -> Option<&mut T> { - Arc::get_mut(&mut self.obj)?.as_any_mut().downcast_mut::() - } - /// 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::() { + styled.map().fmt(f)?; + styled.sub().fmt(f) + } else if let Some(seq) = self.to::() { + 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::() { - if let Some(rhs_mut) = rhs.to_mut::() { - lhs_mut.0.append(&mut rhs_mut.0); - } else if let Some(rhs) = rhs.to::() { - 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::(), rhs.to::()) { - (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 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(&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); - #[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, } /// 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 - 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 - where - Self: Sized; - - /// List the settable properties. - fn properties() -> Vec - where - Self: Sized; - - /// Access a field on this node. - fn field(&self, name: &str) -> Option; + 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() -> Self { - Self(ReadableTypeId::of::()) + pub fn of() -> 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(&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; +} + +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; + + /// List the settable properties. + fn properties() -> Vec; } /// Indicates that a node cannot be labelled. -#[capability] pub trait Unlabellable {} + +#[cold] +#[track_caller] +fn field_is_missing(name: &str) -> ! { + panic!("required field `{name}` is missing") +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 692d18d54..07329e3f3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -13,4 +13,4 @@ pub use self::typeset::*; #[doc(hidden)] pub use once_cell; -pub use typst_macros::{capability, capable, node}; +pub use typst_macros::node; diff --git a/src/model/realize.rs b/src/model/realize.rs index b33cc0bbd..2f38df518 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -1,4 +1,4 @@ -use super::{capability, Content, NodeId, Recipe, Selector, StyleChain, Vt}; +use super::{Content, NodeId, Recipe, Selector, StyleChain, Vt}; use crate::diag::SourceResult; /// Whether the target is affected by show rules in the given style chain. @@ -105,7 +105,7 @@ fn try_apply( let mut result = vec![]; let mut cursor = 0; - for m in regex.find_iter(text) { + for m in regex.find_iter(&text) { let start = m.start(); if cursor < start { result.push(make(text[cursor..start].into())); @@ -133,7 +133,6 @@ fn try_apply( } /// Preparations before execution of any show rule. -#[capability] pub trait Prepare { /// Prepare the node for show rule application. fn prepare( @@ -145,7 +144,6 @@ pub trait Prepare { } /// The base recipe for a node. -#[capability] pub trait Show { /// Execute the base recipe for this node. fn show( @@ -157,7 +155,6 @@ pub trait Show { } /// Post-process a node after it was realized. -#[capability] pub trait Finalize { /// Finalize the fully realized form of the node. Use this for effects that /// should work even in the face of a user-defined show rule, for example diff --git a/src/model/styles.rs b/src/model/styles.rs index 185074917..cbf4cfb23 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -1,21 +1,16 @@ use std::any::Any; use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; +use std::hash::{Hash, Hasher}; use std::iter; use std::marker::PhantomData; -use std::sync::Arc; -use comemo::{Prehashed, Tracked}; +use comemo::Tracked; +use ecow::EcoString; -use super::{Content, Label, NodeId}; +use super::{Content, Label, Node, NodeId}; use crate::diag::{SourceResult, Trace, Tracepoint}; -use crate::eval::{Args, Dict, Func, Regex, Value}; -use crate::geom::{ - Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, - Smart, -}; +use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value}; use crate::syntax::Span; -use crate::util::ReadableTypeId; use crate::World; /// A map of style properties. @@ -76,7 +71,7 @@ impl StyleMap { /// Mark all contained properties as _scoped_. This means that they only /// apply to the first descendant node (of their type) in the hierarchy and /// not its children, too. This is used by - /// [constructors](super::Node::construct). + /// [constructors](super::Construct::construct). pub fn scoped(mut self) -> Self { for entry in &mut self.0 { if let Style::Property(property) = entry { @@ -98,7 +93,7 @@ impl StyleMap { /// Returns `Some(_)` with an optional span if this map contains styles for /// the given `node`. - pub fn interruption(&self) -> Option> { + pub fn interruption(&self) -> Option> { let node = NodeId::of::(); self.0.iter().find_map(|entry| match entry { Style::Property(property) => property.is_of(node).then(|| property.origin), @@ -114,6 +109,12 @@ impl From