diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index bbfe9f7e9..fecc7e336 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -33,10 +33,23 @@ pub struct AlignElem { /// - `horizon` /// - `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 /// the `+` operator to get a `2d alignment`. For example, `top + right` /// 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 /// #set page(height: 6cm) /// #set text(lang: "ar") diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs index 97305ddfd..8d536638b 100644 --- a/crates/typst-library/src/layout/stack.rs +++ b/crates/typst-library/src/layout/stack.rs @@ -26,6 +26,15 @@ pub struct StackElem { /// - `{rtl}`: Right to left. /// - `{ttb}`: Top to bottom. /// - `{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)] pub dir: Dir, diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index f9b95615a..e9bb72ced 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -95,6 +95,9 @@ fn items() -> LangItems { elem.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(), math_align_point: || math::AlignPointElem::new().pack(), math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs index 62a381a91..b14e350bb 100644 --- a/crates/typst-library/src/visualize/line.rs +++ b/crates/typst-library/src/visualize/line.rs @@ -69,6 +69,11 @@ pub struct LineElem { /// the array above), and `phase` (of type [length]($type/length)), /// 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 /// #set line(length: 100%) /// #stack( diff --git a/crates/typst/src/eval/fields.rs b/crates/typst/src/eval/fields.rs new file mode 100644 index 000000000..8c00873b6 --- /dev/null +++ b/crates/typst/src/eval/fields.rs @@ -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 { + 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::() { + 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::>() { + 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"], + _ => &[], + } +} diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs index dcd78b89f..78ae7a593 100644 --- a/crates/typst/src/eval/library.rs +++ b/crates/typst/src/eval/library.rs @@ -6,7 +6,7 @@ use comemo::Tracked; use ecow::EcoString; 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::doc::Document; use crate::geom::{Abs, Dir}; @@ -77,6 +77,12 @@ pub struct LangItems { pub enum_item: fn(number: Option, body: Content) -> Content, /// An item in a term list: `/ Term: Details`. 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 $`. pub equation: fn(body: Content, block: bool) -> Content, /// An alignment point in math: `&`. @@ -144,6 +150,9 @@ impl Hash for LangItems { self.list_item.hash(state); self.enum_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.math_align_point.hash(state); self.math_delimited.hash(state); diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs index 62ac40955..a73684268 100644 --- a/crates/typst/src/eval/methods.rs +++ b/crates/typst/src/eval/methods.rs @@ -1,10 +1,11 @@ //! Methods on values. -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use super::{Args, IntoValue, Str, Value, Vm}; -use crate::diag::{At, SourceResult}; -use crate::eval::Datetime; +use crate::diag::{At, Hint, SourceResult}; +use crate::eval::{bail, Datetime}; +use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign}; use crate::model::{Location, Selector}; use crate::syntax::Span; @@ -24,6 +25,29 @@ pub fn call( "lighten" => color.lighten(args.expect("amount")?).into_value(), "darken" => color.darken(args.expect("amount")?).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(), }, @@ -152,6 +176,30 @@ pub fn call( _ => 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 { "pos" => args.to_pos().into_value(), "named" => args.to_named().into_value(), @@ -198,6 +246,27 @@ pub fn call( "second" => datetime.second().into_value(), _ => return missing(), } + } else if let Some(direction) = dynamic.downcast::() { + 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::() { + match method { + "axis" => align.axis().description().into_value(), + "inv" => align.inv().into_value(), + _ => return missing(), + } + } else if let Some(align2d) = dynamic.downcast::>() { + match method { + "inv" => align2d.map(GenAlign::inv).into_value(), + _ => return missing(), + } } else { 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. pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { 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" => &[ ("len", false), ("at", true), @@ -357,9 +435,16 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ("values", false), ], "function" => &[("where", true), ("with", true)], + "length" => &[("pt", false), ("cm", false), ("mm", false), ("inches", false)], + "angle" => &[("deg", false), ("rad", false)], "arguments" => &[("named", false), ("pos", false)], "location" => &[("page", false), ("position", false), ("page-numbering", false)], "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" => &[ ("display", true), ("at", true), diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index b76765e6e..06f931aa0 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -15,6 +15,7 @@ mod value; mod args; mod auto; mod datetime; +mod fields; mod func; mod int; mod methods; @@ -43,6 +44,7 @@ pub use self::cast::{ }; pub use self::datetime::Datetime; pub use self::dict::{dict, Dict}; +pub use self::fields::fields_on; pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; pub use self::library::{set_lang_items, LangItems, Library}; pub use self::methods::methods_on; diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index b1782cabe..cf866baae 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/eval/value.rs @@ -8,7 +8,7 @@ use ecow::eco_format; use siphasher::sip128::{Hasher128, SipHasher13}; 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, }; use crate::diag::StrResult; @@ -132,7 +132,7 @@ impl Value { Self::Content(content) => content.at(field, None), Self::Module(module) => module.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), } } diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/geom/align.rs index 47acd3a6c..2007db961 100644 --- a/crates/typst/src/geom/align.rs +++ b/crates/typst/src/geom/align.rs @@ -98,6 +98,15 @@ impl GenAlign { 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 for GenAlign { diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/geom/axes.rs index 059d3bb22..1c0848887 100644 --- a/crates/typst/src/geom/axes.rs +++ b/crates/typst/src/geom/axes.rs @@ -178,14 +178,19 @@ impl Axis { 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 { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::X => "horizontal", - Self::Y => "vertical", - }) + f.pad(self.description()) } } diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs index 238c7e681..ab5aa39e7 100644 --- a/crates/typst/src/geom/color.rs +++ b/crates/typst/src/geom/color.rs @@ -1,3 +1,4 @@ +use ecow::{eco_format, EcoString}; use std::str::FromStr; use super::*; @@ -287,6 +288,20 @@ impl RgbaColor { 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 { @@ -335,11 +350,7 @@ impl Debug for RgbaColor { if f.alternate() { write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; } else { - write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; - if self.a != 255 { - write!(f, "{:02x}", self.a)?; - } - write!(f, "\")")?; + write!(f, "rgb(\"{}\")", self.to_hex())?; } Ok(()) } @@ -420,6 +431,13 @@ impl CmykColor { 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 { @@ -442,6 +460,11 @@ impl From for Color { } } +cast! { + CmykColor, + self => Value::Color(self.into()), +} + /// Convert to the closest u8. fn round_u8(value: f64) -> u8 { value.round() as u8 diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/geom/stroke.rs index 66264d5d5..b0387fb71 100644 --- a/crates/typst/src/geom/stroke.rs +++ b/crates/typst/src/geom/stroke.rs @@ -1,4 +1,4 @@ -use crate::eval::{Cast, FromValue}; +use crate::eval::{dict, Cast, FromValue, NoneValue}; use super::*; @@ -159,7 +159,12 @@ impl Debug for PartialStroke { sep = ", "; } 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 = ", "; } 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 cast! { DashPattern, + self => dict! { "array" => self.array, "phase" => self.phase }.into_value(), "solid" => Vec::new().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 -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Clone, Eq, PartialEq, Hash)] pub enum DashLength { LineWidth, Length(T), @@ -369,6 +375,15 @@ impl DashLength { } } +impl Debug for DashLength { + 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 { type Output = DashLength; @@ -382,6 +397,11 @@ impl Resolve for DashLength { cast! { DashLength, + self => match self { + Self::LineWidth => "dot".into_value(), + Self::Length(v) => v.into_value(), + }, + "dot" => Self::LineWidth, v: Length => Self::Length(v), } diff --git a/crates/typst/src/ide/complete.rs b/crates/typst/src/ide/complete.rs index 2c9c386e2..e855dcdb0 100644 --- a/crates/typst/src/ide/complete.rs +++ b/crates/typst/src/ide/complete.rs @@ -7,7 +7,7 @@ use unscanny::Scanner; use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; 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::{ 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 { Value::Symbol(symbol) => { for modifier in symbol.modifiers() { diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/model/content.rs index 373b64203..31106ea54 100644 --- a/crates/typst/src/model/content.rs +++ b/crates/typst/src/model/content.rs @@ -594,12 +594,12 @@ pub trait PlainText { 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] -fn missing_field_no_default(key: &str) -> EcoString { +fn missing_field_no_default(field: &str) -> EcoString { eco_format!( "content does not contain field {:?} and \ no default value was specified", - Str::from(key) + Str::from(field) ) } diff --git a/docs/reference/types.md b/docs/reference/types.md index fe01d4c1e..28646cd20 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -81,13 +81,64 @@ Typst supports the following length units: - Inches: `{1in}` - 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 #rect(width: 20pt) #rect(width: 2em) #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 An angle describing a rotation. Typst supports the following angular units: @@ -100,6 +151,17 @@ Typst supports the following angular units: #rotate(10deg)[Hello there!] ``` +## Methods +### deg() +Converts this angle to degrees. + +- returns: float + +### rad() +Converts this angle to radians. + +- returns: float + # Ratio 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 use a bare length or ratio. +A relative length has the following fields: +- `length`: Its length component. +- `ratio`: Its ratio component. + ## Example ```example #rect(width: 100% - 50pt) + +#(100% - 50pt).length +#(100% - 50pt).ratio ``` # Fraction @@ -155,6 +224,16 @@ Furthermore, Typst provides the following built-in colors: `lime`. ## 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() Lightens a color. @@ -174,6 +253,33 @@ Produces the negative of the 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 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 diff --git a/tests/typ/compiler/field.typ b/tests/typ/compiler/field.typ index dd8499cef..5c28a92cf 100644 --- a/tests/typ/compiler/field.typ +++ b/tests/typ/compiler/field.typ @@ -62,3 +62,89 @@ --- // Error: 9-13 cannot access fields on type boolean #{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) diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index 864ed8ada..c0ad5b1db 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -53,3 +53,148 @@ // Test content fields method. #test([a].fields(), (text: "a")) #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)