diff options
| author | Pg Biel <9021226+PgBiel@users.noreply.github.com> | 2023-07-11 11:11:18 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-11 16:11:18 +0200 |
| commit | 9b1a2b41f0bb2d62106e029a5a0174dcf07ae0d2 (patch) | |
| tree | 6f0ae44b3dfffd1c5b3e44535716d382ffc41c46 /crates | |
| parent | 507efc3a1c14e8487705c7424d605a1663d3fe6e (diff) | |
Add fields and methods to several primitives (#790)
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/typst-library/src/layout/align.rs | 13 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/stack.rs | 9 | ||||
| -rw-r--r-- | crates/typst-library/src/lib.rs | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/line.rs | 5 | ||||
| -rw-r--r-- | crates/typst/src/eval/fields.rs | 93 | ||||
| -rw-r--r-- | crates/typst/src/eval/library.rs | 11 | ||||
| -rw-r--r-- | crates/typst/src/eval/methods.rs | 93 | ||||
| -rw-r--r-- | crates/typst/src/eval/mod.rs | 2 | ||||
| -rw-r--r-- | crates/typst/src/eval/value.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/geom/align.rs | 9 | ||||
| -rw-r--r-- | crates/typst/src/geom/axes.rs | 13 | ||||
| -rw-r--r-- | crates/typst/src/geom/color.rs | 33 | ||||
| -rw-r--r-- | crates/typst/src/geom/stroke.rs | 26 | ||||
| -rw-r--r-- | crates/typst/src/ide/complete.rs | 16 | ||||
| -rw-r--r-- | crates/typst/src/model/content.rs | 6 |
15 files changed, 313 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) ) } |
