Add fields and methods to several primitives (#790)

This commit is contained in:
Pg Biel 2023-07-11 11:11:18 -03:00 committed by GitHub
parent 507efc3a1c
commit 9b1a2b41f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 650 additions and 23 deletions

View File

@ -33,10 +33,23 @@ pub struct AlignElem {
/// - `horizon` /// - `horizon`
/// - `bottom` /// - `bottom`
/// ///
/// You may use the `axis` method on a single-axis alignment to obtain
/// whether it is `{"horizontal"}` or `{"vertical"}`. You may also use the
/// `inv` method to obtain its inverse alignment. For example,
/// `{top.axis()}` is `{"vertical"}`, while `{top.inv()}` is equal to
/// `{bottom}`.
///
/// To align along both axes at the same time, add the two alignments using /// 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` /// the `+` operator to get a `2d alignment`. For example, `top + right`
/// aligns the content to the top right corner. /// aligns the content to the top right corner.
/// ///
/// For 2d alignments, you may use the `x` and `y` fields to access their
/// horizontal and vertical components, respectively. Additionally, you may
/// use the `inv` method to obtain a 2d alignment with both components
/// inverted. For instance, `{(top + right).x}` is `right`,
/// `{(top + right).y}` is `top`, and `{(top + right).inv()}` is equal to
/// `bottom + left`.
///
/// ```example /// ```example
/// #set page(height: 6cm) /// #set page(height: 6cm)
/// #set text(lang: "ar") /// #set text(lang: "ar")

View File

@ -26,6 +26,15 @@ pub struct StackElem {
/// - `{rtl}`: Right to left. /// - `{rtl}`: Right to left.
/// - `{ttb}`: Top to bottom. /// - `{ttb}`: Top to bottom.
/// - `{btt}`: Bottom to top. /// - `{btt}`: Bottom to top.
///
/// You may use the `start` and `end` methods to obtain the initial and
/// final points (respectively) of a direction, as `alignment`. You may
/// also use the `axis` method to obtain whether a direction is
/// `{"horizontal"}` or `{"vertical"}`. Finally, the `inv` method returns
/// its inverse direction.
///
/// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
/// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
#[default(Dir::TTB)] #[default(Dir::TTB)]
pub dir: Dir, pub dir: Dir,

View File

@ -95,6 +95,9 @@ fn items() -> LangItems {
elem.pack() elem.pack()
}, },
term_item: |term, description| layout::TermItem::new(term, description).pack(), term_item: |term, description| layout::TermItem::new(term, description).pack(),
rgb_func: compute::rgb_func(),
cmyk_func: compute::cmyk_func(),
luma_func: compute::luma_func(),
equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), equation: |body, block| math::EquationElem::new(body).with_block(block).pack(),
math_align_point: || math::AlignPointElem::new().pack(), math_align_point: || math::AlignPointElem::new().pack(),
math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(),

View File

@ -69,6 +69,11 @@ pub struct LineElem {
/// the array above), and `phase` (of type [length]($type/length)), /// the array above), and `phase` (of type [length]($type/length)),
/// which defines where in the pattern to start drawing. /// which defines where in the pattern to start drawing.
/// ///
/// Note that, for any `stroke` object, you may access any of the fields
/// mentioned in the dictionary format above. For example,
/// `{(2pt + blue).thickness}` is `{2pt}`, `{(2pt + blue).miter-limit}` is
/// `{4.0}` (the default), and so on.
///
/// ```example /// ```example
/// #set line(length: 100%) /// #set line(length: 100%)
/// #stack( /// #stack(

View File

@ -0,0 +1,93 @@
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::geom::{Axes, GenAlign, PartialStroke, Stroke};
use super::{IntoValue, Value};
/// Try to access a field on a value.
/// This function is exclusively for types which have
/// predefined fields, such as stroke and length.
pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> {
let name = value.type_name();
let not_supported = || Err(no_fields(name));
let missing = || Err(missing_field(name, field));
// Special cases, such as module and dict, are handled by Value itself
let result = match value {
Value::Length(length) => match field {
"em" => length.em.into_value(),
"abs" => length.abs.into_value(),
_ => return missing(),
},
Value::Relative(rel) => match field {
"ratio" => rel.rel.into_value(),
"length" => rel.abs.into_value(),
_ => return missing(),
},
Value::Dyn(dynamic) => {
if let Some(stroke) = dynamic.downcast::<PartialStroke>() {
match field {
"paint" => stroke
.paint
.clone()
.unwrap_or_else(|| Stroke::default().paint)
.into_value(),
"thickness" => stroke
.thickness
.unwrap_or_else(|| Stroke::default().thickness.into())
.into_value(),
"cap" => stroke
.line_cap
.unwrap_or_else(|| Stroke::default().line_cap)
.into_value(),
"join" => stroke
.line_join
.unwrap_or_else(|| Stroke::default().line_join)
.into_value(),
"dash" => stroke.dash_pattern.clone().unwrap_or(None).into_value(),
"miter-limit" => stroke
.miter_limit
.unwrap_or_else(|| Stroke::default().miter_limit)
.0
.into_value(),
_ => return missing(),
}
} else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() {
match field {
"x" => align2d.x.into_value(),
"y" => align2d.y.into_value(),
_ => return missing(),
}
} else {
return not_supported();
}
}
_ => return not_supported(),
};
Ok(result)
}
/// The error message for a type not supporting field access.
#[cold]
fn no_fields(type_name: &str) -> EcoString {
eco_format!("cannot access fields on type {type_name}")
}
/// The missing field error message.
#[cold]
fn missing_field(type_name: &str, field: &str) -> EcoString {
eco_format!("{type_name} does not contain field \"{field}\"")
}
/// List the available fields for a type.
pub fn fields_on(type_name: &str) -> &[&'static str] {
match type_name {
"length" => &["em", "abs"],
"relative length" => &["ratio", "length"],
"stroke" => &["paint", "thickness", "cap", "join", "dash", "miter-limit"],
"2d alignment" => &["x", "y"],
_ => &[],
}
}

View File

@ -6,7 +6,7 @@ use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use std::sync::OnceLock; use std::sync::OnceLock;
use super::{Args, Dynamic, Module, Value, Vm}; use super::{Args, Dynamic, Module, NativeFunc, Value, Vm};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::doc::Document; use crate::doc::Document;
use crate::geom::{Abs, Dir}; use crate::geom::{Abs, Dir};
@ -77,6 +77,12 @@ pub struct LangItems {
pub enum_item: fn(number: Option<usize>, body: Content) -> Content, pub enum_item: fn(number: Option<usize>, body: Content) -> Content,
/// An item in a term list: `/ Term: Details`. /// An item in a term list: `/ Term: Details`.
pub term_item: fn(term: Content, description: Content) -> Content, pub term_item: fn(term: Content, description: Content) -> Content,
/// The constructor for the 'rgba' color kind.
pub rgb_func: &'static NativeFunc,
/// The constructor for the 'cmyk' color kind.
pub cmyk_func: &'static NativeFunc,
/// The constructor for the 'luma' color kind.
pub luma_func: &'static NativeFunc,
/// A mathematical equation: `$x$`, `$ x^2 $`. /// A mathematical equation: `$x$`, `$ x^2 $`.
pub equation: fn(body: Content, block: bool) -> Content, pub equation: fn(body: Content, block: bool) -> Content,
/// An alignment point in math: `&`. /// An alignment point in math: `&`.
@ -144,6 +150,9 @@ impl Hash for LangItems {
self.list_item.hash(state); self.list_item.hash(state);
self.enum_item.hash(state); self.enum_item.hash(state);
self.term_item.hash(state); self.term_item.hash(state);
self.rgb_func.hash(state);
self.cmyk_func.hash(state);
self.luma_func.hash(state);
self.equation.hash(state); self.equation.hash(state);
self.math_align_point.hash(state); self.math_align_point.hash(state);
self.math_delimited.hash(state); self.math_delimited.hash(state);

View File

@ -1,10 +1,11 @@
//! Methods on values. //! Methods on values.
use ecow::EcoString; use ecow::{eco_format, EcoString};
use super::{Args, IntoValue, Str, Value, Vm}; use super::{Args, IntoValue, Str, Value, Vm};
use crate::diag::{At, SourceResult}; use crate::diag::{At, Hint, SourceResult};
use crate::eval::Datetime; use crate::eval::{bail, Datetime};
use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign};
use crate::model::{Location, Selector}; use crate::model::{Location, Selector};
use crate::syntax::Span; use crate::syntax::Span;
@ -24,6 +25,29 @@ pub fn call(
"lighten" => color.lighten(args.expect("amount")?).into_value(), "lighten" => color.lighten(args.expect("amount")?).into_value(),
"darken" => color.darken(args.expect("amount")?).into_value(), "darken" => color.darken(args.expect("amount")?).into_value(),
"negate" => color.negate().into_value(), "negate" => color.negate().into_value(),
"kind" => match color {
Color::Luma(_) => vm.items.luma_func.into_value(),
Color::Rgba(_) => vm.items.rgb_func.into_value(),
Color::Cmyk(_) => vm.items.cmyk_func.into_value(),
},
"hex" => color.to_rgba().to_hex().into_value(),
"rgba" => color.to_rgba().to_array().into_value(),
"cmyk" => match color {
Color::Luma(luma) => luma.to_cmyk().to_array().into_value(),
Color::Rgba(_) => {
bail!(span, "cannot obtain cmyk values from rgba color")
}
Color::Cmyk(cmyk) => cmyk.to_array().into_value(),
},
"luma" => match color {
Color::Luma(luma) => luma.0.into_value(),
Color::Rgba(_) => {
bail!(span, "cannot obtain the luma value of rgba color")
}
Color::Cmyk(_) => {
bail!(span, "cannot obtain the luma value of cmyk color")
}
},
_ => return missing(), _ => return missing(),
}, },
@ -152,6 +176,30 @@ pub fn call(
_ => return missing(), _ => return missing(),
}, },
Value::Length(length) => match method {
unit @ ("pt" | "cm" | "mm" | "inches") => {
if length.em != Em::zero() {
return Err(eco_format!("cannot convert a length with non-zero em units ({length:?}) to {unit}"))
.hint(eco_format!("use 'length.abs.{unit}()' instead to ignore its em component"))
.at(span);
}
match unit {
"pt" => length.abs.to_pt().into_value(),
"cm" => length.abs.to_cm().into_value(),
"mm" => length.abs.to_mm().into_value(),
"inches" => length.abs.to_inches().into_value(),
_ => unreachable!(),
}
}
_ => return missing(),
},
Value::Angle(angle) => match method {
"deg" => angle.to_deg().into_value(),
"rad" => angle.to_rad().into_value(),
_ => return missing(),
},
Value::Args(args) => match method { Value::Args(args) => match method {
"pos" => args.to_pos().into_value(), "pos" => args.to_pos().into_value(),
"named" => args.to_named().into_value(), "named" => args.to_named().into_value(),
@ -198,6 +246,27 @@ pub fn call(
"second" => datetime.second().into_value(), "second" => datetime.second().into_value(),
_ => return missing(), _ => return missing(),
} }
} else if let Some(direction) = dynamic.downcast::<Dir>() {
match method {
"axis" => direction.axis().description().into_value(),
"start" => {
GenAlign::from(Align::from(direction.start())).into_value()
}
"end" => GenAlign::from(Align::from(direction.end())).into_value(),
"inv" => direction.inv().into_value(),
_ => return missing(),
}
} else if let Some(align) = dynamic.downcast::<GenAlign>() {
match method {
"axis" => align.axis().description().into_value(),
"inv" => align.inv().into_value(),
_ => return missing(),
}
} else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() {
match method {
"inv" => align2d.map(GenAlign::inv).into_value(),
_ => return missing(),
}
} else { } else {
return (vm.items.library_method)(vm, &dynamic, method, args, span); return (vm.items.library_method)(vm, &dynamic, method, args, span);
} }
@ -294,7 +363,16 @@ fn missing_method(type_name: &str, method: &str) -> String {
/// List the available methods for a type and whether they take arguments. /// List the available methods for a type and whether they take arguments.
pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
match type_name { match type_name {
"color" => &[("lighten", true), ("darken", true), ("negate", false)], "color" => &[
("lighten", true),
("darken", true),
("negate", false),
("kind", false),
("hex", false),
("rgba", false),
("cmyk", false),
("luma", false),
],
"string" => &[ "string" => &[
("len", false), ("len", false),
("at", true), ("at", true),
@ -357,9 +435,16 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("values", false), ("values", false),
], ],
"function" => &[("where", true), ("with", true)], "function" => &[("where", true), ("with", true)],
"length" => &[("pt", false), ("cm", false), ("mm", false), ("inches", false)],
"angle" => &[("deg", false), ("rad", false)],
"arguments" => &[("named", false), ("pos", false)], "arguments" => &[("named", false), ("pos", false)],
"location" => &[("page", false), ("position", false), ("page-numbering", false)], "location" => &[("page", false), ("position", false), ("page-numbering", false)],
"selector" => &[("or", true), ("and", true), ("before", true), ("after", true)], "selector" => &[("or", true), ("and", true), ("before", true), ("after", true)],
"direction" => {
&[("axis", false), ("start", false), ("end", false), ("inv", false)]
}
"alignment" => &[("axis", false), ("inv", false)],
"2d alignment" => &[("inv", false)],
"counter" => &[ "counter" => &[
("display", true), ("display", true),
("at", true), ("at", true),

View File

@ -15,6 +15,7 @@ mod value;
mod args; mod args;
mod auto; mod auto;
mod datetime; mod datetime;
mod fields;
mod func; mod func;
mod int; mod int;
mod methods; mod methods;
@ -43,6 +44,7 @@ pub use self::cast::{
}; };
pub use self::datetime::Datetime; pub use self::datetime::Datetime;
pub use self::dict::{dict, Dict}; pub use self::dict::{dict, Dict};
pub use self::fields::fields_on;
pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo};
pub use self::library::{set_lang_items, LangItems, Library}; pub use self::library::{set_lang_items, LangItems, Library};
pub use self::methods::methods_on; pub use self::methods::methods_on;

View File

@ -8,7 +8,7 @@ use ecow::eco_format;
use siphasher::sip128::{Hasher128, SipHasher13}; use siphasher::sip128::{Hasher128, SipHasher13};
use super::{ use super::{
cast, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func, cast, fields, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func,
IntoValue, Module, Reflect, Str, Symbol, IntoValue, Module, Reflect, Str, Symbol,
}; };
use crate::diag::StrResult; use crate::diag::StrResult;
@ -132,7 +132,7 @@ impl Value {
Self::Content(content) => content.at(field, None), Self::Content(content) => content.at(field, None),
Self::Module(module) => module.get(field).cloned(), Self::Module(module) => module.get(field).cloned(),
Self::Func(func) => func.get(field).cloned(), Self::Func(func) => func.get(field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())), _ => fields::field(self, field),
} }
} }

View File

@ -98,6 +98,15 @@ impl GenAlign {
Self::Specific(align) => align.axis(), Self::Specific(align) => align.axis(),
} }
} }
/// The inverse alignment.
pub const fn inv(self) -> Self {
match self {
Self::Start => Self::End,
Self::End => Self::Start,
Self::Specific(align) => Self::Specific(align.inv()),
}
}
} }
impl From<Align> for GenAlign { impl From<Align> for GenAlign {

View File

@ -178,14 +178,19 @@ impl Axis {
Self::Y => Self::X, Self::Y => Self::X,
} }
} }
/// A description of this axis' direction.
pub fn description(self) -> &'static str {
match self {
Self::X => "horizontal",
Self::Y => "vertical",
}
}
} }
impl Debug for Axis { impl Debug for Axis {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self { f.pad(self.description())
Self::X => "horizontal",
Self::Y => "vertical",
})
} }
} }

View File

@ -1,3 +1,4 @@
use ecow::{eco_format, EcoString};
use std::str::FromStr; use std::str::FromStr;
use super::*; use super::*;
@ -287,6 +288,20 @@ impl RgbaColor {
a: self.a, a: self.a,
} }
} }
/// Converts this color to a RGB Hex Code.
pub fn to_hex(self) -> EcoString {
if self.a != 255 {
eco_format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
} else {
eco_format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
/// Converts this color to an array of R, G, B, A components.
pub fn to_array(self) -> Array {
array![self.r, self.g, self.b, self.a]
}
} }
impl FromStr for RgbaColor { impl FromStr for RgbaColor {
@ -335,11 +350,7 @@ impl Debug for RgbaColor {
if f.alternate() { if f.alternate() {
write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?;
} else { } else {
write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; write!(f, "rgb(\"{}\")", self.to_hex())?;
if self.a != 255 {
write!(f, "{:02x}", self.a)?;
}
write!(f, "\")")?;
} }
Ok(()) Ok(())
} }
@ -420,6 +431,13 @@ impl CmykColor {
k: self.k, k: self.k,
} }
} }
/// Converts this color to an array of C, M, Y, K components.
pub fn to_array(self) -> Array {
// convert to ratio
let g = |c| Ratio::new(c as f64 / 255.0);
array![g(self.c), g(self.m), g(self.y), g(self.k)]
}
} }
impl Debug for CmykColor { impl Debug for CmykColor {
@ -442,6 +460,11 @@ impl From<CmykColor> for Color {
} }
} }
cast! {
CmykColor,
self => Value::Color(self.into()),
}
/// Convert to the closest u8. /// Convert to the closest u8.
fn round_u8(value: f64) -> u8 { fn round_u8(value: f64) -> u8 {
value.round() as u8 value.round() as u8

View File

@ -1,4 +1,4 @@
use crate::eval::{Cast, FromValue}; use crate::eval::{dict, Cast, FromValue, NoneValue};
use super::*; use super::*;
@ -159,7 +159,12 @@ impl<T: Debug> Debug for PartialStroke<T> {
sep = ", "; sep = ", ";
} }
if let Smart::Custom(dash) = &dash_pattern { if let Smart::Custom(dash) = &dash_pattern {
write!(f, "{}dash: {:?}", sep, dash)?; write!(f, "{}dash: ", sep)?;
if let Some(dash) = dash {
Debug::fmt(dash, f)?;
} else {
Debug::fmt(&NoneValue, f)?;
}
sep = ", "; sep = ", ";
} }
if let Smart::Custom(miter_limit) = &miter_limit { if let Smart::Custom(miter_limit) = &miter_limit {
@ -322,6 +327,7 @@ impl Resolve for DashPattern {
// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns
cast! { cast! {
DashPattern, DashPattern,
self => dict! { "array" => self.array, "phase" => self.phase }.into_value(),
"solid" => Vec::new().into(), "solid" => Vec::new().into(),
"dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(),
@ -348,7 +354,7 @@ cast! {
} }
/// The length of a dash in a line dash pattern /// The length of a dash in a line dash pattern
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
pub enum DashLength<T = Length> { pub enum DashLength<T = Length> {
LineWidth, LineWidth,
Length(T), Length(T),
@ -369,6 +375,15 @@ impl<T> DashLength<T> {
} }
} }
impl<T: Debug> Debug for DashLength<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::LineWidth => write!(f, "\"dot\""),
Self::Length(v) => Debug::fmt(v, f),
}
}
}
impl Resolve for DashLength { impl Resolve for DashLength {
type Output = DashLength<Abs>; type Output = DashLength<Abs>;
@ -382,6 +397,11 @@ impl Resolve for DashLength {
cast! { cast! {
DashLength, DashLength,
self => match self {
Self::LineWidth => "dot".into_value(),
Self::Length(v) => v.into_value(),
},
"dot" => Self::LineWidth, "dot" => Self::LineWidth,
v: Length => Self::Length(v), v: Length => Self::Length(v),
} }

View File

@ -7,7 +7,7 @@ use unscanny::Scanner;
use super::analyze::analyze_labels; use super::analyze::analyze_labels;
use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family};
use crate::doc::Frame; use crate::doc::Frame;
use crate::eval::{format_str, methods_on, CastInfo, Library, Scope, Value}; use crate::eval::{fields_on, format_str, methods_on, CastInfo, Library, Scope, Value};
use crate::syntax::{ use crate::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
}; };
@ -360,6 +360,20 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}) })
} }
for &field in fields_on(value.type_name()) {
// Complete the field name along with its value. Notes:
// 1. No parentheses since function fields cannot currently be called
// with method syntax;
// 2. We can unwrap the field's value since it's a field belonging to
// this value's type, so accessing it should not fail.
ctx.value_completion(
Some(field.into()),
&value.field(field).unwrap(),
false,
None,
);
}
match value { match value {
Value::Symbol(symbol) => { Value::Symbol(symbol) => {
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {

View File

@ -594,12 +594,12 @@ pub trait PlainText {
fn plain_text(&self, text: &mut EcoString); fn plain_text(&self, text: &mut EcoString);
} }
/// The missing key access error message when no default value was given. /// The missing field access error message when no default value was given.
#[cold] #[cold]
fn missing_field_no_default(key: &str) -> EcoString { fn missing_field_no_default(field: &str) -> EcoString {
eco_format!( eco_format!(
"content does not contain field {:?} and \ "content does not contain field {:?} and \
no default value was specified", no default value was specified",
Str::from(key) Str::from(field)
) )
} }

View File

@ -81,13 +81,64 @@ Typst supports the following length units:
- Inches: `{1in}` - Inches: `{1in}`
- Relative to font size: `{2.5em}` - Relative to font size: `{2.5em}`
A length has the following fields:
- `em`: The amount of `em` units in this length, as a [float]($type/float).
- `abs`: A length with just the absolute component of the current length
(that is, excluding the `em` component).
## Example ## Example
```example ```example
#rect(width: 20pt) #rect(width: 20pt)
#rect(width: 2em) #rect(width: 2em)
#rect(width: 1in) #rect(width: 1in)
#(3em + 5pt).em
#(20pt).em
#(40em + 2pt).abs
#(5em).abs
``` ```
## Methods
### pt()
Converts this length to points.
Fails with an error if this length has non-zero `em` units
(such as `5em + 2pt` instead of just `2pt`). Use the `abs`
field (such as in `(5em + 2pt).abs.pt()`) to ignore the
`em` component of the length (thus converting only its
absolute component).
- returns: float
### mm()
Converts this length to millimeters.
Fails with an error if this length has non-zero `em` units
(such as `5em + 2pt` instead of just `2pt`). See the
[`pt()`]($type/float.pt) method for more info.
- returns: float
### cm()
Converts this length to centimeters.
Fails with an error if this length has non-zero `em` units
(such as `5em + 2pt` instead of just `2pt`). See the
[`pt()`]($type/float.pt) method for more info.
- returns: float
### inches()
Converts this length to inches.
Fails with an error if this length has non-zero `em` units
(such as `5em + 2pt` instead of just `2pt`). See the
[`pt()`]($type/float.pt) method for more info.
- returns: float
# Angle # Angle
An angle describing a rotation. An angle describing a rotation.
Typst supports the following angular units: Typst supports the following angular units:
@ -100,6 +151,17 @@ Typst supports the following angular units:
#rotate(10deg)[Hello there!] #rotate(10deg)[Hello there!]
``` ```
## Methods
### deg()
Converts this angle to degrees.
- returns: float
### rad()
Converts this angle to radians.
- returns: float
# Ratio # Ratio
A ratio of a whole. A ratio of a whole.
@ -121,9 +183,16 @@ This type is a combination of a [length]($type/length) with a
of a length and a ratio. Wherever a relative length is expected, you can also of a length and a ratio. Wherever a relative length is expected, you can also
use a bare length or ratio. use a bare length or ratio.
A relative length has the following fields:
- `length`: Its length component.
- `ratio`: Its ratio component.
## Example ## Example
```example ```example
#rect(width: 100% - 50pt) #rect(width: 100% - 50pt)
#(100% - 50pt).length
#(100% - 50pt).ratio
``` ```
# Fraction # Fraction
@ -155,6 +224,16 @@ Furthermore, Typst provides the following built-in colors:
`lime`. `lime`.
## Methods ## Methods
### kind()
Returns the constructor function for this color's kind
([`rgb`]($func/rgb), [`cmyk`]($func/cmyk) or [`luma`]($func/luma)).
```example
#{cmyk(1%, 2%, 3%, 4%).kind() == cmyk}
```
- returns: function
### lighten() ### lighten()
Lightens a color. Lightens a color.
@ -174,6 +253,33 @@ Produces the negative of the color.
- returns: color - returns: color
### hex()
Returns the color's RGB(A) hex representation (such as `#ffaa32` or `#020304fe`).
The alpha component (last two digits in `#020304fe`) is omitted if it is equal
to `ff` (255 / 100%).
- returns: string
### rgba()
Converts this color to sRGB and returns its components (R, G, B, A) as an array
of [integers]($type/integer).
- returns: array
### cmyk()
Converts this color to Digital CMYK and returns its components (C, M, Y, K) as an
array of [ratio]($type/ratio). Note that this function will throw an error when
applied to an [rgb]($func/rgb) color, since its conversion to CMYK is not available.
- returns: array
### luma()
If this color was created with [luma]($func/luma), returns the [integer]($type/integer)
value used on construction. Otherwise (for [rgb]($func/rgb) and [cmyk]($func/cmyk) colors),
throws an error.
- returns: integer
# Datetime # Datetime
Represents a date, a time, or a combination of both. Can be created by either Represents a date, a time, or a combination of both. Can be created by either
specifying a custom datetime using the [`datetime`]($func/datetime) function or specifying a custom datetime using the [`datetime`]($func/datetime) function or

View File

@ -62,3 +62,89 @@
--- ---
// Error: 9-13 cannot access fields on type boolean // Error: 9-13 cannot access fields on type boolean
#{false.true} #{false.true}
---
// Test relative length fields.
#test((100% + 2em + 2pt).ratio, 100%)
#test((100% + 2em + 2pt).length, 2em + 2pt)
#test((100% + 2pt).length, 2pt)
#test((100% + 2pt - 2pt).length, 0pt)
#test((56% + 2pt - 56%).ratio, 0%)
---
// Test length fields.
#test((1pt).em, 0em)
#test((1pt).abs, 1pt)
#test((3em).em, 3em)
#test((3em).abs, 0pt)
#test((2em + 2pt).em, 2em)
#test((2em + 2pt).abs, 2pt)
---
// Test stroke fields for simple strokes.
#test((1em + blue).paint, blue)
#test((1em + blue).thickness, 1em)
#test((1em + blue).cap, "butt")
#test((1em + blue).join, "miter")
#test((1em + blue).dash, none)
#test((1em + blue).miter-limit, 4.0)
---
// Test complex stroke fields.
#let r1 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: none))
#let r2 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: (3pt, "dot", 4em)))
#let r3 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", dash: (array: (3pt, "dot", 4em), phase: 5em)))
#let s1 = r1.stroke
#let s2 = r2.stroke
#let s3 = r3.stroke
#test(s1.paint, cmyk(1%, 2%, 3%, 4%))
#test(s1.thickness, 4em + 2pt)
#test(s1.cap, "round")
#test(s1.join, "bevel")
#test(s1.miter-limit, 5.0)
#test(s3.miter-limit, 4.0)
#test(s1.dash, none)
#test(s2.dash, (array: (3pt, "dot", 4em), phase: 0pt))
#test(s3.dash, (array: (3pt, "dot", 4em), phase: 5em))
---
// Test 2d alignment 'horizontal' field.
#test((start + top).x, start)
#test((end + top).x, end)
#test((left + top).x, left)
#test((right + top).x, right)
#test((center + top).x, center)
#test((start + bottom).x, start)
#test((end + bottom).x, end)
#test((left + bottom).x, left)
#test((right + bottom).x, right)
#test((center + bottom).x, center)
#test((start + horizon).x, start)
#test((end + horizon).x, end)
#test((left + horizon).x, left)
#test((right + horizon).x, right)
#test((center + horizon).x, center)
#test((top + start).x, start)
#test((bottom + end).x, end)
#test((horizon + center).x, center)
---
// Test 2d alignment 'vertical' field.
#test((start + top).y, top)
#test((end + top).y, top)
#test((left + top).y, top)
#test((right + top).y, top)
#test((center + top).y, top)
#test((start + bottom).y, bottom)
#test((end + bottom).y, bottom)
#test((left + bottom).y, bottom)
#test((right + bottom).y, bottom)
#test((center + bottom).y, bottom)
#test((start + horizon).y, horizon)
#test((end + horizon).y, horizon)
#test((left + horizon).y, horizon)
#test((right + horizon).y, horizon)
#test((center + horizon).y, horizon)
#test((top + start).y, top)
#test((bottom + end).y, bottom)
#test((horizon + center).y, horizon)

View File

@ -53,3 +53,148 @@
// Test content fields method. // Test content fields method.
#test([a].fields(), (text: "a")) #test([a].fields(), (text: "a"))
#test([a *b*].fields(), (children: ([a], [ ], strong[b]))) #test([a *b*].fields(), (children: ([a], [ ], strong[b])))
---
// Test length unit conversions.
#test((500.934pt).pt(), 500.934)
#test((3.3453cm).cm(), 3.3453)
#test((4.3452mm).mm(), 4.3452)
#test((5.345in).inches(), 5.345)
#test((500.333666999pt).pt(), 500.333666999)
#test((3.5234354cm).cm(), 3.5234354)
#test((4.12345678mm).mm(), 4.12345678)
#test((5.333666999in).inches(), 5.333666999)
#test((4.123456789123456mm).mm(), 4.123456789123456)
#test((254cm).mm(), 2540.0)
#test(calc.round((254cm).inches(), digits: 2), 100.0)
#test((2540mm).cm(), 254.0)
#test(calc.round((2540mm).inches(), digits: 2), 100.0)
#test((100in).pt(), 7200.0)
#test(calc.round((100in).cm(), digits: 2), 254.0)
#test(calc.round((100in).mm(), digits: 2), 2540.0)
#test(5em.abs.cm(), 0.0)
#test((5em + 6in).abs.inches(), 6.0)
---
// Error: 2-21 cannot convert a length with non-zero em units (-6pt + 10.5em) to pt
// Hint: 2-21 use 'length.abs.pt()' instead to ignore its em component
#(10.5em - 6pt).pt()
---
// Error: 2-12 cannot convert a length with non-zero em units (3em) to cm
// Hint: 2-12 use 'length.abs.cm()' instead to ignore its em component
#(3em).cm()
---
// Error: 2-20 cannot convert a length with non-zero em units (-226.77pt + 93em) to mm
// Hint: 2-20 use 'length.abs.mm()' instead to ignore its em component
#(93em - 80mm).mm()
---
// Error: 2-24 cannot convert a length with non-zero em units (432pt + 4.5em) to inches
// Hint: 2-24 use 'length.abs.inches()' instead to ignore its em component
#(4.5em + 6in).inches()
---
// Test color kind method.
#test(rgb(1, 2, 3, 4).kind(), rgb)
#test(cmyk(4%, 5%, 6%, 7%).kind(), cmyk)
#test(luma(40).kind(), luma)
#test(rgb(1, 2, 3, 4).kind() != luma, true)
---
// Test color '.rgba()', '.cmyk()' and '.luma()' without conversions
#test(rgb(1, 2, 3, 4).rgba(), (1, 2, 3, 4))
#test(rgb(1, 2, 3).rgba(), (1, 2, 3, 255))
#test(cmyk(20%, 20%, 40%, 20%).cmyk(), (20%, 20%, 40%, 20%))
#test(luma(40).luma(), 40)
---
// Test color conversions.
#test(rgb(1, 2, 3).hex(), "#010203")
#test(rgb(1, 2, 3, 4).hex(), "#01020304")
#test(cmyk(4%, 5%, 6%, 7%).rgba(), (228, 225, 223, 255))
#test(cmyk(4%, 5%, 6%, 7%).hex(), "#e4e1df")
#test(luma(40).rgba(), (40, 40, 40, 255))
#test(luma(40).hex(), "#282828")
#test(repr(luma(40).cmyk()), repr((11.76%, 10.59%, 10.59%, 14.12%)))
---
// Error: 2-24 cannot obtain cmyk values from rgba color
#rgb(1, 2, 3, 4).cmyk()
---
// Error: 2-24 cannot obtain the luma value of rgba color
#rgb(1, 2, 3, 4).luma()
---
// Error: 2-29 cannot obtain the luma value of cmyk color
#cmyk(4%, 5%, 6%, 7%).luma()
---
// Test alignment methods.
#test(start.axis(), "horizontal")
#test(end.axis(), "horizontal")
#test(left.axis(), "horizontal")
#test(right.axis(), "horizontal")
#test(center.axis(), "horizontal")
#test(top.axis(), "vertical")
#test(bottom.axis(), "vertical")
#test(horizon.axis(), "vertical")
#test(start.inv(), end)
#test(end.inv(), start)
#test(left.inv(), right)
#test(right.inv(), left)
#test(center.inv(), center)
#test(top.inv(), bottom)
#test(bottom.inv(), top)
#test(horizon.inv(), horizon)
---
// Test 2d alignment methods.
#test((start + top).inv(), (end + bottom))
#test((end + top).inv(), (start + bottom))
#test((left + top).inv(), (right + bottom))
#test((right + top).inv(), (left + bottom))
#test((center + top).inv(), (center + bottom))
#test((start + bottom).inv(), (end + top))
#test((end + bottom).inv(), (start + top))
#test((left + bottom).inv(), (right + top))
#test((right + bottom).inv(), (left + top))
#test((center + bottom).inv(), (center + top))
#test((start + horizon).inv(), (end + horizon))
#test((end + horizon).inv(), (start + horizon))
#test((left + horizon).inv(), (right + horizon))
#test((right + horizon).inv(), (left + horizon))
#test((center + horizon).inv(), (center + horizon))
#test((top + start).inv(), (end + bottom))
#test((bottom + end).inv(), (start + top))
#test((horizon + center).inv(), (center + horizon))
---
// Test direction methods.
#test(ltr.axis(), "horizontal")
#test(rtl.axis(), "horizontal")
#test(ttb.axis(), "vertical")
#test(btt.axis(), "vertical")
#test(ltr.start(), left)
#test(rtl.start(), right)
#test(ttb.start(), top)
#test(btt.start(), bottom)
#test(ltr.end(), right)
#test(rtl.end(), left)
#test(ttb.end(), bottom)
#test(btt.end(), top)
#test(ltr.inv(), rtl)
#test(rtl.inv(), ltr)
#test(ttb.inv(), btt)
#test(btt.inv(), ttb)
---
// Test angle methods.
#test(1rad.rad(), 1.0)
#test(1.23rad.rad(), 1.23)
#test(0deg.rad(), 0.0)
#test(2deg.deg(), 2.0)
#test(2.94deg.deg(), 2.94)
#test(0rad.deg(), 0.0)