summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPg Biel <9021226+PgBiel@users.noreply.github.com>2023-07-11 11:11:18 -0300
committerGitHub <noreply@github.com>2023-07-11 16:11:18 +0200
commit9b1a2b41f0bb2d62106e029a5a0174dcf07ae0d2 (patch)
tree6f0ae44b3dfffd1c5b3e44535716d382ffc41c46
parent507efc3a1c14e8487705c7424d605a1663d3fe6e (diff)
Add fields and methods to several primitives (#790)
-rw-r--r--crates/typst-library/src/layout/align.rs13
-rw-r--r--crates/typst-library/src/layout/stack.rs9
-rw-r--r--crates/typst-library/src/lib.rs3
-rw-r--r--crates/typst-library/src/visualize/line.rs5
-rw-r--r--crates/typst/src/eval/fields.rs93
-rw-r--r--crates/typst/src/eval/library.rs11
-rw-r--r--crates/typst/src/eval/methods.rs93
-rw-r--r--crates/typst/src/eval/mod.rs2
-rw-r--r--crates/typst/src/eval/value.rs4
-rw-r--r--crates/typst/src/geom/align.rs9
-rw-r--r--crates/typst/src/geom/axes.rs13
-rw-r--r--crates/typst/src/geom/color.rs33
-rw-r--r--crates/typst/src/geom/stroke.rs26
-rw-r--r--crates/typst/src/ide/complete.rs16
-rw-r--r--crates/typst/src/model/content.rs6
-rw-r--r--docs/reference/types.md106
-rw-r--r--tests/typ/compiler/field.typ86
-rw-r--r--tests/typ/compiler/methods.typ145
18 files changed, 650 insertions, 23 deletions
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
index bbfe9f7e..fecc7e33 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 97305ddf..8d536638 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 f9b95615..e9bb72ce 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 62a381a9..b14e350b 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 00000000..8c00873b
--- /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<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"],
+ _ => &[],
+ }
+}
diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs
index dcd78b89..78ae7a59 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<usize>, 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 62ac4095..a7368426 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::<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 {
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 b76765e6..06f931aa 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 b1782cab..cf866baa 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 47acd3a6..2007db96 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<Align> for GenAlign {
diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/geom/axes.rs
index 059d3bb2..1c084888 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 238c7e68..ab5aa39e 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<CmykColor> 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 66264d5d..b0387fb7 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<T: Debug> Debug for PartialStroke<T> {
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<T = Length> {
LineWidth,
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 {
type Output = DashLength<Abs>;
@@ -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 2c9c386e..e855dcdb 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 373b6420..31106ea5 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 fe01d4c1..28646cd2 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 dd8499ce..5c28a92c 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 864ed8ad..c0ad5b1d 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)