diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/foundations | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/foundations')
34 files changed, 14527 insertions, 0 deletions
diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs new file mode 100644 index 00000000..ee282a87 --- /dev/null +++ b/crates/typst-library/src/foundations/args.rs @@ -0,0 +1,453 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value, +}; + +/// Captured arguments to a function. +/// +/// # Argument Sinks +/// Like built-in functions, custom functions can also take a variable number of +/// arguments. You can specify an _argument sink_ which collects all excess +/// arguments as `..sink`. The resulting `sink` value is of the `arguments` +/// type. It exposes methods to access the positional and named arguments. +/// +/// ```example +/// #let format(title, ..authors) = { +/// let by = authors +/// .pos() +/// .join(", ", last: " and ") +/// +/// [*#title* \ _Written by #by;_] +/// } +/// +/// #format("ArtosFlow", "Jane", "Joe") +/// ``` +/// +/// # Spreading +/// Inversely to an argument sink, you can _spread_ arguments, arrays and +/// dictionaries into a function call with the `..spread` operator: +/// +/// ```example +/// #let array = (2, 3, 5) +/// #calc.min(..array) +/// #let dict = (fill: blue) +/// #text(..dict)[Hello] +/// ``` +#[ty(scope, cast, name = "arguments")] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Args { + /// The callsite span for the function. This is not the span of the argument + /// list itself, but of the whole function call. + pub span: Span, + /// The positional and named arguments. + pub items: EcoVec<Arg>, +} + +impl Args { + /// Create positional arguments from a span and values. + pub fn new<T: IntoValue>(span: Span, values: impl IntoIterator<Item = T>) -> Self { + let items = values + .into_iter() + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value.into_value(), span), + }) + .collect(); + Self { span, items } + } + + /// Attach a span to these arguments if they don't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Returns the number of remaining positional arguments. + pub fn remaining(&self) -> usize { + self.items.iter().filter(|slot| slot.name.is_none()).count() + } + + /// Insert a positional argument at a specific index. + pub fn insert(&mut self, index: usize, span: Span, value: Value) { + self.items.insert( + index, + Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }, + ) + } + + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + + /// Consume and cast the first positional argument if there is one. + pub fn eat<T>(&mut self) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Consume n positional arguments if possible. + pub fn consume(&mut self, n: usize) -> SourceResult<Vec<Arg>> { + let mut list = vec![]; + + let mut i = 0; + while i < self.items.len() && list.len() < n { + if self.items[i].name.is_none() { + list.push(self.items.remove(i)); + } else { + i += 1; + } + } + + if list.len() < n { + bail!(self.span, "not enough arguments"); + } + + Ok(list) + } + + /// Consume and cast the first positional argument. + /// + /// Returns a `missing argument: {what}` error if no positional argument is + /// left. + pub fn expect<T>(&mut self, what: &str) -> SourceResult<T> + where + T: FromValue<Spanned<Value>>, + { + match self.eat()? { + Some(v) => Ok(v), + None => bail!(self.missing_argument(what)), + } + } + + /// The error message for missing arguments. + fn missing_argument(&self, what: &str) -> SourceDiagnostic { + for item in &self.items { + let Some(name) = item.name.as_deref() else { continue }; + if name == what { + return error!( + item.span, + "the argument `{what}` is positional"; + hint: "try removing `{}:`", name, + ); + } + } + + error!(self.span, "missing argument: {what}") + } + + /// Find and consume the first castable positional argument. + pub fn find<T>(&mut self) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() && T::castable(&slot.value.v) { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Find and consume all castable positional arguments. + pub fn all<T>(&mut self) -> SourceResult<Vec<T>> + where + T: FromValue<Spanned<Value>>, + { + let mut list = vec![]; + let mut errors = eco_vec![]; + self.items.retain(|item| { + if item.name.is_some() { + return true; + }; + let span = item.value.span; + let spanned = Spanned::new(std::mem::take(&mut item.value.v), span); + match T::from_value(spanned).at(span) { + Ok(val) => list.push(val), + Err(diags) => errors.extend(diags), + } + false + }); + if !errors.is_empty() { + return Err(errors); + } + Ok(list) + } + + /// Cast and remove the value for the given named argument, returning an + /// error if the conversion fails. + pub fn named<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + // We don't quit once we have a match because when multiple matches + // exist, we want to remove all of them and use the last one. + let mut i = 0; + let mut found = None; + while i < self.items.len() { + if self.items[i].name.as_deref() == Some(name) { + let value = self.items.remove(i).value; + let span = value.span; + found = Some(T::from_value(value).at(span)?); + } else { + i += 1; + } + } + Ok(found) + } + + /// Same as named, but with fallback to find. + pub fn named_or_find<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + match self.named(name)? { + Some(value) => Ok(Some(value)), + None => self.find(), + } + } + + /// Take out all arguments into a new instance. + pub fn take(&mut self) -> Self { + Self { + span: self.span, + items: std::mem::take(&mut self.items), + } + } + + /// Return an "unexpected argument" error if there is any remaining + /// argument. + pub fn finish(self) -> SourceResult<()> { + if let Some(arg) = self.items.first() { + match &arg.name { + Some(name) => bail!(arg.span, "unexpected argument: {name}"), + _ => bail!(arg.span, "unexpected argument"), + } + } + Ok(()) + } +} + +/// A key that can be used to get an argument: either the index of a positional +/// argument, or the name of a named argument. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ArgumentKey { + Index(i64), + Name(Str), +} + +cast! { + ArgumentKey, + v: i64 => Self::Index(v), + v: Str => Self::Name(v), +} + +impl Args { + fn get(&self, key: &ArgumentKey) -> Option<&Value> { + let item = match key { + &ArgumentKey::Index(index) => { + let mut iter = self.items.iter().filter(|item| item.name.is_none()); + if index < 0 { + let index = (-(index + 1)).try_into().ok()?; + iter.nth_back(index) + } else { + let index = index.try_into().ok()?; + iter.nth(index) + } + } + // Accept the last argument with the right name. + ArgumentKey::Name(name) => { + self.items.iter().rfind(|item| item.name.as_ref() == Some(name)) + } + }; + item.map(|item| &item.value.v) + } +} + +#[scope] +impl Args { + /// Construct spreadable arguments in place. + /// + /// This function behaves like `{let args(..sink) = sink}`. + /// + /// ```example + /// #let args = arguments(stroke: red, inset: 1em, [Body]) + /// #box(..args) + /// ``` + #[func(constructor)] + pub fn construct( + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The arguments to construct. + #[external] + #[variadic] + arguments: Vec<Value>, + ) -> Args { + args.take() + } + + /// Returns the positional argument at the specified index, or the named + /// argument with the specified name. + /// + /// If the key is an [integer]($int), this is equivalent to first calling + /// [`pos`]($arguments.pos) and then [`array.at`]. If it is a [string]($str), + /// this is equivalent to first calling [`named`]($arguments.named) and then + /// [`dictionary.at`]. + #[func] + pub fn at( + &self, + /// The index or name of the argument to get. + key: ArgumentKey, + /// A default value to return if the key is invalid. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(key)) + } + + /// Returns the captured positional arguments as an array. + #[func(name = "pos", title = "Positional")] + pub fn to_pos(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Returns the captured named arguments as a dictionary. + #[func(name = "named")] + pub fn to_named(&self) -> Dict { + self.items + .iter() + .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) + .collect() + } +} + +impl Debug for Args { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_list().entries(&self.items).finish() + } +} + +impl Repr for Args { + fn repr(&self) -> EcoString { + let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>(); + repr::pretty_array_like(&pieces, false).into() + } +} + +impl PartialEq for Args { + fn eq(&self, other: &Self) -> bool { + self.to_pos() == other.to_pos() && self.to_named() == other.to_named() + } +} + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Arg { + /// The span of the whole argument. + pub span: Span, + /// The name of the argument (`None` for positional arguments). + pub name: Option<Str>, + /// The value of the argument. + pub value: Spanned<Value>, +} + +impl Debug for Arg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(name) = &self.name { + name.fmt(f)?; + f.write_str(": ")?; + self.value.v.fmt(f) + } else { + self.value.v.fmt(f) + } + } +} + +impl Repr for Arg { + fn repr(&self) -> EcoString { + if let Some(name) = &self.name { + eco_format!("{}: {}", name, self.value.v.repr()) + } else { + self.value.v.repr() + } + } +} + +impl PartialEq for Arg { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value.v == other.value.v + } +} + +/// Things that can be used as arguments. +pub trait IntoArgs { + /// Convert into arguments, attaching the `fallback` span in case `Self` + /// doesn't have a span. + fn into_args(self, fallback: Span) -> Args; +} + +impl IntoArgs for Args { + fn into_args(self, fallback: Span) -> Args { + self.spanned(fallback) + } +} + +impl<I, T> IntoArgs for I +where + I: IntoIterator<Item = T>, + T: IntoValue, +{ + fn into_args(self, fallback: Span) -> Args { + Args::new(fallback, self) + } +} + +/// The missing key access error message when no default was given. +#[cold] +fn missing_key_no_default(key: ArgumentKey) -> EcoString { + eco_format!( + "arguments do not contain key {} \ + and no default value was specified", + match key { + ArgumentKey::Index(i) => i.repr(), + ArgumentKey::Name(name) => name.repr(), + } + ) +} diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs new file mode 100644 index 00000000..9c8aecac --- /dev/null +++ b/crates/typst-library/src/foundations/array.rs @@ -0,0 +1,1133 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Formatter}; +use std::num::{NonZeroI64, NonZeroUsize}; +use std::ops::{Add, AddAssign}; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, + Func, IntoValue, Reflect, Repr, Str, Value, Version, +}; + +/// Create a new [`Array`] from values. +#[macro_export] +#[doc(hidden)] +macro_rules! __array { + ($value:expr; $count:expr) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![ + $crate::foundations::IntoValue::into_value($value); + $count + ]) + }; + + ($($value:expr),* $(,)?) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![$( + $crate::foundations::IntoValue::into_value($value) + ),*]) + }; +} + +#[doc(inline)] +pub use crate::__array as array; + +/// A sequence of values. +/// +/// You can construct an array by enclosing a comma-separated sequence of values +/// in parentheses. The values do not have to be of the same type. +/// +/// You can access and update array items with the `.at()` method. Indices are +/// zero-based and negative indices wrap around to the end of the array. You can +/// iterate over an array using a [for loop]($scripting/#loops). Arrays can be +/// added together with the `+` operator, [joined together]($scripting/#blocks) +/// and multiplied with integers. +/// +/// **Note:** An array of length one needs a trailing comma, as in `{(1,)}`. +/// This is to disambiguate from a simple parenthesized expressions like `{(1 + +/// 2) * 3}`. An empty array is written as `{()}`. +/// +/// # Example +/// ```example +/// #let values = (1, 7, 4, -3, 2) +/// +/// #values.at(0) \ +/// #(values.at(0) = 3) +/// #values.at(-1) \ +/// #values.find(calc.even) \ +/// #values.filter(calc.odd) \ +/// #values.map(calc.abs) \ +/// #values.rev() \ +/// #(1, (2, 3)).flatten() \ +/// #(("A", "B", "C") +/// .join(", ", last: " and ")) +/// ``` +#[ty(scope, cast)] +#[derive(Default, Clone, PartialEq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Array(EcoVec<Value>); + +impl Array { + /// Create a new, empty array. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new vec, with a known capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(EcoVec::with_capacity(capacity)) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() + } + + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter<Value> { + self.0.iter() + } + + /// Mutably borrow the first value in the array. + pub fn first_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().first_mut().ok_or_else(array_is_empty) + } + + /// Mutably borrow the last value in the array. + pub fn last_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().last_mut().ok_or_else(array_is_empty) + } + + /// Mutably borrow the value at the given index. + pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { + let len = self.len(); + self.locate_opt(index, false) + .and_then(move |i| self.0.make_mut().get_mut(i)) + .ok_or_else(|| out_of_bounds(index, len)) + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> { + self.locate_opt(index, end_ok) + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds if and only if `end_ok` is true. + fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v < self.0.len() + end_ok as usize) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + let count = self + .len() + .checked_mul(n) + .ok_or_else(|| format!("cannot repeat this array {n} times"))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } +} + +#[scope] +impl Array { + /// Converts a value to an array. + /// + /// Note that this function is only intended for conversion of a collection-like + /// value to an array, not for creation of an array from individual items. Use + /// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead. + /// + /// ```example + /// #let hi = "Hello 😃" + /// #array(bytes(hi)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an array. + value: ToArray, + ) -> Array { + value.0 + } + + /// The number of values in the array. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the first item in the array. May be used on the left-hand side + /// of an assignment. Fails with an error if the array is empty. + #[func] + pub fn first(&self) -> StrResult<Value> { + self.0.first().cloned().ok_or_else(array_is_empty) + } + + /// Returns the last item in the array. May be used on the left-hand side of + /// an assignment. Fails with an error if the array is empty. + #[func] + pub fn last(&self) -> StrResult<Value> { + self.0.last().cloned().ok_or_else(array_is_empty) + } + + /// Returns the item at the specified index in the array. May be used on the + /// left-hand side of an assignment. Returns the default value if the index + /// is out of bounds or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index, false) + .and_then(|i| self.0.get(i).cloned()) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Adds a value to the end of the array. + #[func] + pub fn push( + &mut self, + /// The value to insert at the end of the array. + value: Value, + ) { + self.0.push(value); + } + + /// Removes the last item from the array and returns it. Fails with an error + /// if the array is empty. + #[func] + pub fn pop(&mut self) -> StrResult<Value> { + self.0.pop().ok_or_else(array_is_empty) + } + + /// Inserts a value into the array at the specified index, shifting all + /// subsequent elements to the right. Fails with an error if the index is + /// out of bounds. + /// + /// To replace an element of an array, use [`at`]($array.at). + #[func] + pub fn insert( + &mut self, + /// The index at which to insert the item. If negative, indexes from + /// the back. + index: i64, + /// The value to insert into the array. + value: Value, + ) -> StrResult<()> { + let i = self.locate(index, true)?; + self.0.insert(i, value); + Ok(()) + } + + /// Removes the value at the specified index from the array and return it. + #[func] + pub fn remove( + &mut self, + /// The index at which to remove the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index, false) + .map(|i| self.0.remove(i)) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the array. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). If negative, indexes from the back. + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// of the array is extracted. If negative, indexes from the back. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Array> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start, true)?; + let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start); + Ok(self.0[start..end].into()) + } + + /// Whether the array contains the specified value. + /// + /// This method also has dedicated syntax: You can write `{2 in (1, 2, 3)}` + /// instead of `{(1, 2, 3).contains(2)}`. + #[func] + pub fn contains( + &self, + /// The value to search for. + value: Value, + ) -> bool { + self.0.contains(&value) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the first match or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<Value>> { + for item in self.iter() { + if searcher + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(searcher.span())? + { + return Ok(Some(item.clone())); + } + } + Ok(None) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the index of the first match or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<i64>> { + for (i, item) in self.iter().enumerate() { + if searcher + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(searcher.span())? + { + return Ok(Some(i as i64)); + } + } + + Ok(None) + } + + /// Create an array consisting of a sequence of numbers. + /// + /// If you pass just one positional parameter, it is interpreted as the + /// `end` of the range. If you pass two, they describe the `start` and `end` + /// of the range. + /// + /// This function is available both in the array function's scope and + /// globally. + /// + /// ```example + /// #range(5) \ + /// #range(2, 5) \ + /// #range(20, step: 4) \ + /// #range(21, step: 4) \ + /// #range(5, 2, step: -1) + /// ``` + #[func] + pub fn range( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The start of the range (inclusive). + #[external] + #[default] + start: i64, + /// The end of the range (exclusive). + #[external] + end: i64, + /// The distance between the generated numbers. + #[named] + #[default(NonZeroI64::new(1).unwrap())] + step: NonZeroI64, + ) -> SourceResult<Array> { + let first = args.expect::<i64>("end")?; + let (start, end) = match args.eat::<i64>()? { + Some(second) => (first, second), + None => (0, first), + }; + + let step = step.get(); + + let mut x = start; + let mut array = Self::new(); + + while x.cmp(&end) == 0.cmp(&step) { + array.push(x.into_value()); + x += step; + } + + Ok(array) + } + + /// Produces a new array with only the items from the original one for which + /// the given function returns true. + #[func] + pub fn filter( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<Array> { + let mut kept = EcoVec::new(); + for item in self.iter() { + if test + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(test.span())? + { + kept.push(item.clone()) + } + } + Ok(kept.into()) + } + + /// Produces a new array in which all items from the original one were + /// transformed with the given function. + #[func] + pub fn map( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. + mapper: Func, + ) -> SourceResult<Array> { + self.into_iter() + .map(|item| mapper.call(engine, context, [item])) + .collect() + } + + /// Returns a new array with the values alongside their indices. + /// + /// The returned array consists of `(index, value)` pairs in the form of + /// length-2 arrays. These can be [destructured]($scripting/#bindings) with + /// a let binding or for loop. + #[func] + pub fn enumerate( + self, + /// The index returned for the first pair of the returned list. + #[named] + #[default(0)] + start: i64, + ) -> StrResult<Array> { + self.into_iter() + .enumerate() + .map(|(i, value)| { + Ok(array![ + start + .checked_add_unsigned(i as u64) + .ok_or("array index is too large")?, + value + ] + .into_value()) + }) + .collect() + } + + /// Zips the array with other arrays. + /// + /// Returns an array of arrays, where the `i`th inner array contains all the + /// `i`th elements from each original array. + /// + /// If the arrays to be zipped have different lengths, they are zipped up to + /// the last element of the shortest array and all remaining elements are + /// ignored. + /// + /// This function is variadic, meaning that you can zip multiple arrays + /// together at once: `{(1, 2).zip(("A", "B"), (10, 20))}` yields + /// `{((1, "A", 10), (2, "B", 20))}`. + #[func] + pub fn zip( + self, + /// The real arguments (the `others` arguments are just for the docs, this + /// function is a bit involved, so we parse the positional arguments manually). + args: &mut Args, + /// Whether all arrays have to have the same length. + /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an + /// error. + #[named] + #[default(false)] + exact: bool, + /// The arrays to zip with. + #[external] + #[variadic] + others: Vec<Array>, + ) -> SourceResult<Array> { + let remaining = args.remaining(); + + // Fast path for one array. + if remaining == 0 { + return Ok(self.into_iter().map(|item| array![item].into_value()).collect()); + } + + // Fast path for just two arrays. + if remaining == 1 { + let Spanned { v: other, span: other_span } = + args.expect::<Spanned<Array>>("others")?; + if exact && self.len() != other.len() { + bail!( + other_span, + "second array has different length ({}) from first array ({})", + other.len(), + self.len() + ); + } + return Ok(self + .into_iter() + .zip(other) + .map(|(first, second)| array![first, second].into_value()) + .collect()); + } + + // If there is more than one array, we use the manual method. + let mut out = Self::with_capacity(self.len()); + let arrays = args.all::<Spanned<Array>>()?; + if exact { + let errs = arrays + .iter() + .filter(|sp| sp.v.len() != self.len()) + .map(|Spanned { v, span }| { + SourceDiagnostic::error( + *span, + eco_format!( + "array has different length ({}) from first array ({})", + v.len(), + self.len() + ), + ) + }) + .collect::<EcoVec<_>>(); + if !errs.is_empty() { + return Err(errs); + } + } + + let mut iterators = + arrays.into_iter().map(|i| i.v.into_iter()).collect::<Vec<_>>(); + + for this in self { + let mut row = Self::with_capacity(1 + iterators.len()); + row.push(this.clone()); + + for iterator in &mut iterators { + let Some(item) = iterator.next() else { + return Ok(out); + }; + + row.push(item); + } + + out.push(row.into_value()); + } + + Ok(out) + } + + /// Folds all items into a single value using an accumulator function. + #[func] + pub fn fold( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The initial value to start with. + init: Value, + /// The folding function. Must have two parameters: One for the + /// accumulated value and one for an item. + folder: Func, + ) -> SourceResult<Value> { + let mut acc = init; + for item in self { + acc = folder.call(engine, context, [acc, item])?; + } + Ok(acc) + } + + /// Sums all items (works for all types that can be added). + #[func] + pub fn sum( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> HintedStrResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate sum of empty array with no default")?; + for item in iter { + acc = ops::add(acc, item)?; + } + Ok(acc) + } + + /// Calculates the product all items (works for all types that can be + /// multiplied). + #[func] + pub fn product( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> HintedStrResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate product of empty array with no default")?; + for item in iter { + acc = ops::mul(acc, item)?; + } + Ok(acc) + } + + /// Whether the given function returns `{true}` for any item in the array. + #[func] + pub fn any( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { + for item in self { + if test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether the given function returns `{true}` for all items in the array. + #[func] + pub fn all( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { + for item in self { + if !test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { + return Ok(false); + } + } + + Ok(true) + } + + /// Combine all nested arrays into a single flat one. + #[func] + pub fn flatten(self) -> Array { + let mut flat = EcoVec::with_capacity(self.0.len()); + for item in self { + if let Value::Array(nested) = item { + flat.extend(nested.flatten()); + } else { + flat.push(item); + } + } + flat.into() + } + + /// Return a new array with the same items, but in reverse order. + #[func(title = "Reverse")] + pub fn rev(self) -> Array { + self.into_iter().rev().collect() + } + + /// Split the array at occurrences of the specified value. + #[func] + pub fn split( + &self, + /// The value to split at. + at: Value, + ) -> Array { + self.as_slice() + .split(|value| *value == at) + .map(|subslice| Value::Array(subslice.iter().cloned().collect())) + .collect() + } + + /// Combine all items in the array into one. + #[func] + pub fn join( + self, + /// A value to insert between each item of the array. + #[default] + separator: Option<Value>, + /// An alternative separator between the last two items. + #[named] + last: Option<Value>, + ) -> StrResult<Value> { + let len = self.0.len(); + let separator = separator.unwrap_or(Value::None); + + let mut last = last; + let mut result = Value::None; + for (i, value) in self.into_iter().enumerate() { + if i > 0 { + if i + 1 == len && last.is_some() { + result = ops::join(result, last.take().unwrap())?; + } else { + result = ops::join(result, separator.clone())?; + } + } + + result = ops::join(result, value)?; + } + + Ok(result) + } + + /// Returns an array with a copy of the separator value placed between + /// adjacent elements. + #[func] + pub fn intersperse( + self, + /// The value that will be placed between each adjacent element. + separator: Value, + ) -> Array { + // TODO: Use once stabilized: + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse + let size = match self.len() { + 0 => return Array::new(), + n => (2 * n) - 1, + }; + let mut vec = EcoVec::with_capacity(size); + let mut iter = self.into_iter(); + + if let Some(first) = iter.next() { + vec.push(first); + } + + for value in iter { + vec.push(separator.clone()); + vec.push(value); + } + + Array(vec) + } + + /// Splits an array into non-overlapping chunks, starting at the beginning, + /// ending with a single remainder chunk. + /// + /// All chunks but the last have `chunk-size` elements. + /// If `exact` is set to `{true}`, the remainder is dropped if it + /// contains less than `chunk-size` elements. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.chunks(3) + /// #array.chunks(3, exact: true) + /// ``` + #[func] + pub fn chunks( + self, + /// How many elements each chunk may at most contain. + chunk_size: NonZeroUsize, + /// Whether to keep the remainder if its size is less than `chunk-size`. + #[named] + #[default(false)] + exact: bool, + ) -> Array { + let to_array = |chunk| Array::from(chunk).into_value(); + if exact { + self.0.chunks_exact(chunk_size.get()).map(to_array).collect() + } else { + self.0.chunks(chunk_size.get()).map(to_array).collect() + } + } + + /// Returns sliding windows of `window-size` elements over an array. + /// + /// If the array length is less than `window-size`, this will return an empty array. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.windows(5) + /// ``` + #[func] + pub fn windows( + self, + /// How many elements each window will contain. + window_size: NonZeroUsize, + ) -> Array { + self.0 + .windows(window_size.get()) + .map(|window| Array::from(window).into_value()) + .collect() + } + + /// Return a sorted version of this array, optionally by a given key + /// function. The sorting algorithm used is stable. + /// + /// Returns an error if two values could not be compared or if the key + /// function (if given) yields an error. + #[func] + pub fn sorted( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// If given, applies this function to the elements in the array to + /// determine the keys to sort by. + #[named] + key: Option<Func>, + ) -> SourceResult<Array> { + let mut result = Ok(()); + let mut vec = self.0; + let mut key_of = |x: Value| match &key { + // NOTE: We are relying on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the `key`. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + vec.make_mut().sort_by(|a, b| { + // Until we get `try` blocks :) + match (key_of(a.clone()), key_of(b.clone())) { + (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { + if result.is_ok() { + result = Err(err).at(span); + } + Ordering::Equal + }), + (Err(e), _) | (_, Err(e)) => { + if result.is_ok() { + result = Err(e); + } + Ordering::Equal + } + } + }); + result.map(|_| vec.into()) + } + + /// Deduplicates all items in the array. + /// + /// Returns a new array with all duplicate items removed. Only the first + /// element of each duplicate is kept. + /// + /// ```example + /// #(1, 1, 2, 3, 1).dedup() + /// ``` + #[func(title = "Deduplicate")] + pub fn dedup( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// If given, applies this function to the elements in the array to + /// determine the keys to deduplicate by. + #[named] + key: Option<Func>, + ) -> SourceResult<Array> { + let mut out = EcoVec::with_capacity(self.0.len()); + let mut key_of = |x: Value| match &key { + // NOTE: We are relying on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the `key`. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + + // This algorithm is O(N^2) because we cannot rely on `HashSet` since: + // 1. We would like to preserve the order of the elements. + // 2. We cannot hash arbitrary `Value`. + 'outer: for value in self { + let key = key_of(value.clone())?; + if out.is_empty() { + out.push(value); + continue; + } + + for second in out.iter() { + if ops::equal(&key, &key_of(second.clone())?) { + continue 'outer; + } + } + + out.push(value); + } + + Ok(Self(out)) + } + + /// Converts an array of pairs into a dictionary. + /// The first value of each pair is the key, the second the value. + /// + /// If the same key occurs multiple times, the last value is selected. + /// + /// ```example + /// #( + /// ("apples", 2), + /// ("peaches", 3), + /// ("apples", 5), + /// ).to-dict() + /// ``` + #[func] + pub fn to_dict(self) -> StrResult<Dict> { + self.into_iter() + .map(|value| { + let value_ty = value.ty(); + let pair = value.cast::<Array>().map_err(|_| { + eco_format!("expected (str, any) pairs, found {}", value_ty) + })?; + if let [key, value] = pair.as_slice() { + let key = key.clone().cast::<Str>().map_err(|_| { + eco_format!("expected key of type str, found {}", value.ty()) + })?; + Ok((key, value.clone())) + } else { + bail!("expected pairs of length 2, found length {}", pair.len()); + } + }) + .collect() + } + + /// Reduces the elements to a single one, by repeatedly applying a reducing + /// operation. + /// + /// If the array is empty, returns `{none}`, otherwise, returns the result + /// of the reduction. + /// + /// The reducing function is a closure with two arguments: an "accumulator", + /// and an element. + /// + /// For arrays with at least one element, this is the same as [`array.fold`] + /// with the first element of the array as the initial accumulator value, + /// folding every subsequent element into it. + #[func] + pub fn reduce( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The reducing function. Must have two parameters: One for the + /// accumulated value and one for an item. + reducer: Func, + ) -> SourceResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter.next().unwrap_or_default(); + for item in iter { + acc = reducer.call(engine, context, [acc, item])?; + } + Ok(acc) + } +} + +/// A value that can be cast to bytes. +pub struct ToArray(Array); + +cast! { + ToArray, + v: Array => Self(v), + v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()), + v: Version => Self(v.values().iter().map(|&v| Value::Int(v as i64)).collect()) +} + +impl Debug for Array { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.0).finish() + } +} + +impl Repr for Array { + fn repr(&self) -> EcoString { + let max = 40; + let mut pieces: Vec<_> = self + .iter() + .take(max) + .map(|value| eco_format!("{}", value.repr())) + .collect(); + if self.len() > max { + pieces.push(eco_format!(".. ({} items omitted)", self.len() - max)); + } + repr::pretty_array_like(&pieces, self.len() == 1).into() + } +} + +impl Add for Array { + type Output = Self; + + fn add(mut self, rhs: Array) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Array { + fn add_assign(&mut self, rhs: Self) { + self.0.extend(rhs.0); + } +} + +impl Extend<Value> for Array { + fn extend<T: IntoIterator<Item = Value>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl FromIterator<Value> for Array { + fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl IntoIterator for Array { + type Item = Value; + type IntoIter = ecow::vec::IntoIter<Value>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Array { + type Item = &'a Value; + type IntoIter = std::slice::Iter<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl From<EcoVec<Value>> for Array { + fn from(v: EcoVec<Value>) -> Self { + Array(v) + } +} + +impl From<&[Value]> for Array { + fn from(v: &[Value]) -> Self { + Array(v.into()) + } +} + +impl<T> Reflect for Vec<T> { + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: Reflect, const N: usize> Reflect for SmallVec<[T; N]> { + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Vec<T> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl<T: IntoValue, const N: usize> IntoValue for SmallVec<[T; N]> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl<T: FromValue> FromValue for Vec<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + +impl<T: FromValue, const N: usize> FromValue for SmallVec<[T; N]> { + fn from_value(value: Value) -> HintedStrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + +/// The error message when the array is empty. +#[cold] +fn array_is_empty() -> EcoString { + "array is empty".into() +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("array index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "array index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst-library/src/foundations/auto.rs b/crates/typst-library/src/foundations/auto.rs new file mode 100644 index 00000000..8237b450 --- /dev/null +++ b/crates/typst-library/src/foundations/auto.rs @@ -0,0 +1,267 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type, + Value, +}; + +/// A value that indicates a smart default. +/// +/// The auto type has exactly one value: `{auto}`. +/// +/// Parameters that support the `{auto}` value have some smart default or +/// contextual behaviour. A good example is the [text direction]($text.dir) +/// parameter. Setting it to `{auto}` lets Typst automatically determine the +/// direction from the [text language]($text.lang). +#[ty(cast, name = "auto")] +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AutoValue; + +impl IntoValue for AutoValue { + fn into_value(self) -> Value { + Value::Auto + } +} + +impl FromValue for AutoValue { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Auto") + } +} + +impl Repr for AutoValue { + fn repr(&self) -> EcoString { + "auto".into() + } +} + +/// A value that can be automatically determined. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Smart<T> { + /// The value should be determined smartly based on the circumstances. + Auto, + /// A specific value. + Custom(T), +} + +impl<T> Smart<T> { + /// Whether the value is `Auto`. + pub fn is_auto(&self) -> bool { + matches!(self, Self::Auto) + } + + /// Whether this holds a custom value. + pub fn is_custom(&self) -> bool { + matches!(self, Self::Custom(_)) + } + + /// Whether this is a `Smart::Custom(x)` and `f(x)` is true. + pub fn is_custom_and<F>(self, f: F) -> bool + where + F: Fn(T) -> bool, + { + match self { + Self::Auto => false, + Self::Custom(x) => f(x), + } + } + + /// Returns a `Smart<&T>` borrowing the inner `T`. + pub fn as_ref(&self) -> Smart<&T> { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(v) => Smart::Custom(v), + } + } + + /// Returns the contained custom value. + /// + /// If the value is [`Smart::Auto`], returns `None`. + /// + /// Equivalently, this just converts `Smart` to `Option`. + pub fn custom(self) -> Option<T> { + match self { + Self::Auto => None, + Self::Custom(x) => Some(x), + } + } + + /// Map the contained custom value with `f`. + pub fn map<F, U>(self, f: F) -> Smart<U> + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => Smart::Auto, + Self::Custom(x) => Smart::Custom(f(x)), + } + } + + /// Map the contained custom value with `f` if it contains a custom value, + /// otherwise returns `default`. + pub fn map_or<F, U>(self, default: U, f: F) -> U + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => default, + Self::Custom(x) => f(x), + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns `other`. + pub fn or(self, other: Smart<T>) -> Self { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => other, + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns the + /// output of the given function. + pub fn or_else<F>(self, f: F) -> Self + where + F: FnOnce() -> Self, + { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => f(), + } + } + + /// Returns `Auto` if `self` is `Auto`, otherwise calls the provided + /// function on the contained value and returns the result. + pub fn and_then<F, U>(self, f: F) -> Smart<U> + where + F: FnOnce(T) -> Smart<U>, + { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(x) => f(x), + } + } + + /// Returns the contained custom value or a provided default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Self::Auto => default, + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or computes a default value. + pub fn unwrap_or_else<F>(self, f: F) -> T + where + F: FnOnce() -> T, + { + match self { + Self::Auto => f(), + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or the default value. + pub fn unwrap_or_default(self) -> T + where + T: Default, + { + // we want to do this; the Clippy lint is not type-aware + #[allow(clippy::unwrap_or_default)] + self.unwrap_or_else(T::default) + } +} + +impl<T> Smart<Smart<T>> { + /// Removes a single level of nesting, returns `Auto` if the inner or outer value is `Auto`. + pub fn flatten(self) -> Smart<T> { + match self { + Smart::Custom(Smart::Auto) | Smart::Auto => Smart::Auto, + Smart::Custom(Smart::Custom(v)) => Smart::Custom(v), + } + } +} + +impl<T> Default for Smart<T> { + fn default() -> Self { + Self::Auto + } +} + +impl<T: Reflect> Reflect for Smart<T> { + fn input() -> CastInfo { + T::input() + AutoValue::input() + } + + fn output() -> CastInfo { + T::output() + AutoValue::output() + } + + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Smart<T> { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, + } + } +} + +impl<T: FromValue> FromValue for Smart<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} + +impl<T: Resolve> Resolve for Smart<T> { + type Output = Smart<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl<T: Fold> Fold for Smart<T> { + fn fold(self, outer: Self) -> Self { + use Smart::Custom; + match (self, outer) { + (Custom(inner), Custom(outer)) => Custom(inner.fold(outer)), + // An explicit `auto` should be respected, thus we don't do + // `inner.or(outer)`. + (inner, _) => inner, + } + } +} diff --git a/crates/typst-library/src/foundations/bool.rs b/crates/typst-library/src/foundations/bool.rs new file mode 100644 index 00000000..e88c8c6f --- /dev/null +++ b/crates/typst-library/src/foundations/bool.rs @@ -0,0 +1,26 @@ +use ecow::EcoString; + +use crate::foundations::{ty, Repr}; + +/// A type with two states. +/// +/// The boolean type has two values: `{true}` and `{false}`. It denotes whether +/// something is active or enabled. +/// +/// # Example +/// ```example +/// #false \ +/// #true \ +/// #(1 < 2) +/// ``` +#[ty(cast, title = "Boolean")] +type bool; + +impl Repr for bool { + fn repr(&self) -> EcoString { + match self { + true => "true".into(), + false => "false".into(), + } + } +} diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs new file mode 100644 index 00000000..05fe4763 --- /dev/null +++ b/crates/typst-library/src/foundations/bytes.rs @@ -0,0 +1,262 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign, Deref}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use typst_utils::LazyHash; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; + +/// A sequence of bytes. +/// +/// This is conceptually similar to an array of [integers]($int) between `{0}` +/// and `{255}`, but represented much more efficiently. You can iterate over it +/// using a [for loop]($scripting/#loops). +/// +/// You can convert +/// - a [string]($str) or an [array] of integers to bytes with the [`bytes`] +/// constructor +/// - bytes to a string with the [`str`] constructor, with UTF-8 encoding +/// - bytes to an array of integers with the [`array`] constructor +/// +/// When [reading]($read) data from a file, you can decide whether to load it +/// as a string or as raw bytes. +/// +/// ```example +/// #bytes((123, 160, 22, 0)) \ +/// #bytes("Hello 😃") +/// +/// #let data = read( +/// "rhino.png", +/// encoding: none, +/// ) +/// +/// // Magic bytes. +/// #array(data.slice(0, 4)) \ +/// #str(data.slice(1, 4)) +/// ``` +#[ty(scope, cast)] +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>); + +impl Bytes { + /// Create a buffer from a static byte slice. + pub fn from_static(slice: &'static [u8]) -> Self { + Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return a view into the buffer. + pub fn as_slice(&self) -> &[u8] { + self + } + + /// Return a copy of the buffer as a vector. + pub fn to_vec(&self) -> Vec<u8> { + self.0.to_vec() + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult<usize> { + self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> Option<usize> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()) + } +} + +#[scope] +impl Bytes { + /// Converts a value to bytes. + /// + /// - Strings are encoded in UTF-8. + /// - Arrays of integers between `{0}` and `{255}` are converted directly. The + /// dedicated byte representation is much more efficient than the array + /// representation and thus typically used for large byte buffers (e.g. image + /// data). + /// + /// ```example + /// #bytes("Hello 😃") \ + /// #bytes((123, 160, 22, 0)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to bytes. + value: ToBytes, + ) -> Bytes { + value.0 + } + + /// The length in bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the byte at the specified index. Returns the default value if + /// the index is out of bounds or fails with an error if no default value + /// was specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the byte. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index) + .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the bytes. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// is extracted. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with + /// `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Bytes> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + Ok(self.0[start..end].into()) + } +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Bytes({})", self.len()) + } +} + +impl Repr for Bytes { + fn repr(&self) -> EcoString { + eco_format!("bytes({})", self.len()) + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl From<&[u8]> for Bytes { + fn from(slice: &[u8]) -> Self { + Self(Arc::new(LazyHash::new(slice.to_vec().into()))) + } +} + +impl From<Vec<u8>> for Bytes { + fn from(vec: Vec<u8>) -> Self { + Self(Arc::new(LazyHash::new(vec.into()))) + } +} + +impl Add for Bytes { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Bytes { + fn add_assign(&mut self, rhs: Self) { + if rhs.is_empty() { + // Nothing to do + } else if self.is_empty() { + *self = rhs; + } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { + Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else { + *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + } + } +} + +impl Serialize for Bytes { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&eco_format!("{self:?}")) + } else { + serializer.serialize_bytes(self) + } + } +} + +/// A value that can be cast to bytes. +pub struct ToBytes(Bytes); + +cast! { + ToBytes, + v: Str => Self(v.as_bytes().into()), + v: Array => Self(v.iter() + .map(|item| match item { + Value::Int(byte @ 0..=255) => Ok(*byte as u8), + Value::Int(_) => bail!("number must be between 0 and 255"), + value => Err(<u8 as Reflect>::error(value)), + }) + .collect::<Result<Vec<u8>, _>>()? + .into() + ), + v: Bytes => Self(v), +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("byte index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "byte index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs new file mode 100644 index 00000000..f12ca74c --- /dev/null +++ b/crates/typst-library/src/foundations/calc.rs @@ -0,0 +1,1211 @@ +//! Calculations and processing of numeric values. + +use std::cmp; +use std::cmp::Ordering; + +use az::SaturatingAs; +use typst_syntax::{Span, Spanned}; +use typst_utils::{round_int_with_precision, round_with_precision}; + +use crate::diag::{bail, At, HintedString, SourceResult, StrResult}; +use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value}; +use crate::layout::{Angle, Fr, Length, Ratio}; + +/// A module with calculation definitions. +pub fn module() -> Module { + let mut scope = Scope::new(); + scope.define_func::<abs>(); + scope.define_func::<pow>(); + scope.define_func::<exp>(); + scope.define_func::<sqrt>(); + scope.define_func::<root>(); + scope.define_func::<sin>(); + scope.define_func::<cos>(); + scope.define_func::<tan>(); + scope.define_func::<asin>(); + scope.define_func::<acos>(); + scope.define_func::<atan>(); + scope.define_func::<atan2>(); + scope.define_func::<sinh>(); + scope.define_func::<cosh>(); + scope.define_func::<tanh>(); + scope.define_func::<log>(); + scope.define_func::<ln>(); + scope.define_func::<fact>(); + scope.define_func::<perm>(); + scope.define_func::<binom>(); + scope.define_func::<gcd>(); + scope.define_func::<lcm>(); + scope.define_func::<floor>(); + scope.define_func::<ceil>(); + scope.define_func::<trunc>(); + scope.define_func::<fract>(); + scope.define_func::<round>(); + scope.define_func::<clamp>(); + scope.define_func::<min>(); + scope.define_func::<max>(); + scope.define_func::<even>(); + scope.define_func::<odd>(); + scope.define_func::<rem>(); + scope.define_func::<div_euclid>(); + scope.define_func::<rem_euclid>(); + scope.define_func::<quo>(); + scope.define("inf", f64::INFINITY); + scope.define("pi", std::f64::consts::PI); + scope.define("tau", std::f64::consts::TAU); + scope.define("e", std::f64::consts::E); + Module::new("calc", scope) +} + +/// Calculates the absolute value of a numeric value. +/// +/// ```example +/// #calc.abs(-5) \ +/// #calc.abs(5pt - 2cm) \ +/// #calc.abs(2fr) \ +/// #calc.abs(decimal("-342.440")) +/// ``` +#[func(title = "Absolute")] +pub fn abs( + /// The value whose absolute value to calculate. + value: ToAbs, +) -> Value { + value.0 +} + +/// A value of which the absolute value can be taken. +pub struct ToAbs(Value); + +cast! { + ToAbs, + v: i64 => Self(v.abs().into_value()), + v: f64 => Self(v.abs().into_value()), + v: Length => Self(Value::Length(v.try_abs() + .ok_or("cannot take absolute value of this length")?)), + v: Angle => Self(Value::Angle(v.abs())), + v: Ratio => Self(Value::Ratio(v.abs())), + v: Fr => Self(Value::Fraction(v.abs())), + v: Decimal => Self(Value::Decimal(v.abs())) +} + +/// Raises a value to some exponent. +/// +/// ```example +/// #calc.pow(2, 3) \ +/// #calc.pow(decimal("2.5"), 2) +/// ``` +#[func(title = "Power")] +pub fn pow( + /// The callsite span. + span: Span, + /// The base of the power. + /// + /// If this is a [`decimal`], the exponent can only be an [integer]($int). + base: DecNum, + /// The exponent of the power. + exponent: Spanned<Num>, +) -> SourceResult<DecNum> { + match exponent.v { + _ if exponent.v.float() == 0.0 && base.is_zero() => { + bail!(span, "zero to the power of zero is undefined") + } + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + }; + + match (base, exponent.v) { + (DecNum::Int(a), Num::Int(b)) if b >= 0 => a + .checked_pow(b as u32) + .map(DecNum::Int) + .ok_or_else(too_large) + .at(span), + (DecNum::Decimal(a), Num::Int(b)) => { + a.checked_powi(b).map(DecNum::Decimal).ok_or_else(too_large).at(span) + } + (a, b) => { + let Some(a) = a.float() else { + return Err(cant_apply_to_decimal_and_float()).at(span); + }; + + let result = if a == std::f64::consts::E { + b.float().exp() + } else if a == 2.0 { + b.float().exp2() + } else if let Num::Int(b) = b { + a.powi(b as i32) + } else { + a.powf(b.float()) + }; + + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(DecNum::Float(result)) + } + } +} + +/// Raises a value to some exponent of e. +/// +/// ```example +/// #calc.exp(1) +/// ``` +#[func(title = "Exponential")] +pub fn exp( + /// The callsite span. + span: Span, + /// The exponent of the power. + exponent: Spanned<Num>, +) -> SourceResult<f64> { + match exponent.v { + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + } + + let result = exponent.v.float().exp(); + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the square root of a number. +/// +/// ```example +/// #calc.sqrt(16) \ +/// #calc.sqrt(2.5) +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The number whose square root to calculate. Must be non-negative. + value: Spanned<Num>, +) -> SourceResult<f64> { + if value.v.float() < 0.0 { + bail!(value.span, "cannot take square root of negative number"); + } + Ok(value.v.float().sqrt()) +} + +/// Calculates the real nth root of a number. +/// +/// If the number is negative, then n must be odd. +/// +/// ```example +/// #calc.root(16.0, 4) \ +/// #calc.root(27.0, 3) +/// ``` +#[func] +pub fn root( + /// The expression to take the root of + radicand: f64, + /// Which root of the radicand to take + index: Spanned<i64>, +) -> SourceResult<f64> { + if index.v == 0 { + bail!(index.span, "cannot take the 0th root of a number"); + } else if radicand < 0.0 { + if index.v % 2 == 0 { + bail!( + index.span, + "negative numbers do not have a real nth root when n is even" + ); + } else { + Ok(-(-radicand).powf(1.0 / index.v as f64)) + } + } else { + Ok(radicand.powf(1.0 / index.v as f64)) + } +} + +/// Calculates the sine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.sin(1.5) \ +/// #calc.sin(90deg) +/// ``` +#[func(title = "Sine")] +pub fn sin( + /// The angle whose sine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.sin(), + AngleLike::Int(n) => (n as f64).sin(), + AngleLike::Float(n) => n.sin(), + } +} + +/// Calculates the cosine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.cos(1.5) \ +/// #calc.cos(90deg) +/// ``` +#[func(title = "Cosine")] +pub fn cos( + /// The angle whose cosine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.cos(), + AngleLike::Int(n) => (n as f64).cos(), + AngleLike::Float(n) => n.cos(), + } +} + +/// Calculates the tangent of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.tan(1.5) \ +/// #calc.tan(90deg) +/// ``` +#[func(title = "Tangent")] +pub fn tan( + /// The angle whose tangent to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.tan(), + AngleLike::Int(n) => (n as f64).tan(), + AngleLike::Float(n) => n.tan(), + } +} + +/// Calculates the arcsine of a number. +/// +/// ```example +/// #calc.asin(0) \ +/// #calc.asin(1) +/// ``` +#[func(title = "Arcsine")] +pub fn asin( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned<Num>, +) -> SourceResult<Angle> { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.asin())) +} + +/// Calculates the arccosine of a number. +/// +/// ```example +/// #calc.acos(0) \ +/// #calc.acos(1) +/// ``` +#[func(title = "Arccosine")] +pub fn acos( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned<Num>, +) -> SourceResult<Angle> { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.acos())) +} + +/// Calculates the arctangent of a number. +/// +/// ```example +/// #calc.atan(0) \ +/// #calc.atan(1) +/// ``` +#[func(title = "Arctangent")] +pub fn atan( + /// The number whose arctangent to calculate. + value: Num, +) -> Angle { + Angle::rad(value.float().atan()) +} + +/// Calculates the four-quadrant arctangent of a coordinate. +/// +/// The arguments are `(x, y)`, not `(y, x)`. +/// +/// ```example +/// #calc.atan2(1, 1) \ +/// #calc.atan2(-2, -3) +/// ``` +#[func(title = "Four-quadrant Arctangent")] +pub fn atan2( + /// The X coordinate. + x: Num, + /// The Y coordinate. + y: Num, +) -> Angle { + Angle::rad(f64::atan2(y.float(), x.float())) +} + +/// Calculates the hyperbolic sine of a hyperbolic angle. +/// +/// ```example +/// #calc.sinh(0) \ +/// #calc.sinh(1.5) +/// ``` +#[func(title = "Hyperbolic Sine")] +pub fn sinh( + /// The hyperbolic angle whose hyperbolic sine to calculate. + value: f64, +) -> f64 { + value.sinh() +} + +/// Calculates the hyperbolic cosine of a hyperbolic angle. +/// +/// ```example +/// #calc.cosh(0) \ +/// #calc.cosh(1.5) +/// ``` +#[func(title = "Hyperbolic Cosine")] +pub fn cosh( + /// The hyperbolic angle whose hyperbolic cosine to calculate. + value: f64, +) -> f64 { + value.cosh() +} + +/// Calculates the hyperbolic tangent of an hyperbolic angle. +/// +/// ```example +/// #calc.tanh(0) \ +/// #calc.tanh(1.5) +/// ``` +#[func(title = "Hyperbolic Tangent")] +pub fn tanh( + /// The hyperbolic angle whose hyperbolic tangent to calculate. + value: f64, +) -> f64 { + value.tanh() +} + +/// Calculates the logarithm of a number. +/// +/// If the base is not specified, the logarithm is calculated in base 10. +/// +/// ```example +/// #calc.log(100) +/// ``` +#[func(title = "Logarithm")] +pub fn log( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, + /// The base of the logarithm. May not be zero. + #[named] + #[default(Spanned::new(10.0, Span::detached()))] + base: Spanned<f64>, +) -> SourceResult<f64> { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + if !base.v.is_normal() { + bail!(base.span, "base may not be zero, NaN, infinite, or subnormal") + } + + let result = if base.v == std::f64::consts::E { + number.ln() + } else if base.v == 2.0 { + number.log2() + } else if base.v == 10.0 { + number.log10() + } else { + number.log(base.v) + }; + + if result.is_infinite() || result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the natural logarithm of a number. +/// +/// ```example +/// #calc.ln(calc.e) +/// ``` +#[func(title = "Natural Logarithm")] +pub fn ln( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, +) -> SourceResult<f64> { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + let result = number.ln(); + if result.is_infinite() { + bail!(span, "result close to -inf") + } + + Ok(result) +} + +/// Calculates the factorial of a number. +/// +/// ```example +/// #calc.fact(5) +/// ``` +#[func(title = "Factorial")] +pub fn fact( + /// The number whose factorial to calculate. Must be non-negative. + number: u64, +) -> StrResult<i64> { + Ok(fact_impl(1, number).ok_or_else(too_large)?) +} + +/// Calculates a permutation. +/// +/// Returns the `k`-permutation of `n`, or the number of ways to choose `k` +/// items from a set of `n` with regard to order. +/// +/// ```example +/// $ "perm"(n, k) &= n!/((n - k)!) \ +/// "perm"(5, 3) &= #calc.perm(5, 3) $ +/// ``` +#[func(title = "Permutation")] +pub fn perm( + /// The base number. Must be non-negative. + base: u64, + /// The number of permutations. Must be non-negative. + numbers: u64, +) -> StrResult<i64> { + // By convention. + if base < numbers { + return Ok(0); + } + + Ok(fact_impl(base - numbers + 1, base).ok_or_else(too_large)?) +} + +/// Calculates the product of a range of numbers. Used to calculate +/// permutations. Returns None if the result is larger than `i64::MAX` +fn fact_impl(start: u64, end: u64) -> Option<i64> { + // By convention + if end + 1 < start { + return Some(0); + } + + let real_start: u64 = cmp::max(1, start); + let mut count: u64 = 1; + for i in real_start..=end { + count = count.checked_mul(i)?; + } + + count.try_into().ok() +} + +/// Calculates a binomial coefficient. +/// +/// Returns the `k`-combination of `n`, or the number of ways to choose `k` +/// items from a set of `n` without regard to order. +/// +/// ```example +/// #calc.binom(10, 5) +/// ``` +#[func(title = "Binomial")] +pub fn binom( + /// The upper coefficient. Must be non-negative. + n: u64, + /// The lower coefficient. Must be non-negative. + k: u64, +) -> StrResult<i64> { + Ok(binom_impl(n, k).ok_or_else(too_large)?) +} + +/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` +/// the lower coefficient. Returns `None` if the result is larger than +/// `i64::MAX` +fn binom_impl(n: u64, k: u64) -> Option<i64> { + if k > n { + return Some(0); + } + + // By symmetry + let real_k = cmp::min(n - k, k); + if real_k == 0 { + return Some(1); + } + + let mut result: u64 = 1; + for i in 0..real_k { + result = result.checked_mul(n - i)?.checked_div(i + 1)?; + } + + result.try_into().ok() +} + +/// Calculates the greatest common divisor of two integers. +/// +/// ```example +/// #calc.gcd(7, 42) +/// ``` +#[func(title = "Greatest Common Divisor")] +pub fn gcd( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> i64 { + let (mut a, mut b) = (a, b); + while b != 0 { + let temp = b; + b = a % b; + a = temp; + } + + a.abs() +} + +/// Calculates the least common multiple of two integers. +/// +/// ```example +/// #calc.lcm(96, 13) +/// ``` +#[func(title = "Least Common Multiple")] +pub fn lcm( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> StrResult<i64> { + if a == b { + return Ok(a.abs()); + } + + Ok(a.checked_div(gcd(a, b)) + .and_then(|gcd| gcd.checked_mul(b)) + .map(|v| v.abs()) + .ok_or_else(too_large)?) +} + +/// Rounds a number down to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.floor(500.1) +/// #assert(calc.floor(3) == 3) +/// #assert(calc.floor(3.14) == 3) +/// #assert(calc.floor(decimal("-3.14")) == -4) +/// ``` +#[func] +pub fn floor( + /// The number to round down. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.floor()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.floor()).map_err(|_| too_large())?), + } +} + +/// Rounds a number up to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.ceil(500.1) +/// #assert(calc.ceil(3) == 3) +/// #assert(calc.ceil(3.14) == 4) +/// #assert(calc.ceil(decimal("-3.14")) == -3) +/// ``` +#[func] +pub fn ceil( + /// The number to round up. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.ceil()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.ceil()).map_err(|_| too_large())?), + } +} + +/// Returns the integer part of a number. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.trunc(15.9) +/// #assert(calc.trunc(3) == 3) +/// #assert(calc.trunc(-3.7) == -3) +/// #assert(calc.trunc(decimal("8493.12949582390")) == 8493) +/// ``` +#[func(title = "Truncate")] +pub fn trunc( + /// The number to truncate. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.trunc()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.trunc()).map_err(|_| too_large())?), + } +} + +/// Returns the fractional part of a number. +/// +/// If the number is an integer, returns `0`. +/// +/// ```example +/// #calc.fract(-3.1) +/// #assert(calc.fract(3) == 0) +/// #assert(calc.fract(decimal("234.23949211")) == decimal("0.23949211")) +/// ``` +#[func(title = "Fractional")] +pub fn fract( + /// The number to truncate. + value: DecNum, +) -> DecNum { + match value { + DecNum::Int(_) => DecNum::Int(0), + DecNum::Float(n) => DecNum::Float(n.fract()), + DecNum::Decimal(n) => DecNum::Decimal(n.fract()), + } +} + +/// Rounds a number to the nearest integer away from zero. +/// +/// Optionally, a number of decimal places can be specified. +/// +/// If the number of digits is negative, its absolute value will indicate the +/// amount of significant integer digits to remove before the decimal point. +/// +/// Note that this function will return the same type as the operand. That is, +/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], +/// another `decimal`. You may explicitly convert the output of this function to +/// an integer with [`int`], but note that such a conversion will error if the +/// `float` or `decimal` is larger than the maximum 64-bit signed integer or +/// smaller than the minimum integer. +/// +/// In addition, this function can error if there is an attempt to round beyond +/// the maximum or minimum integer or `decimal`. If the number is a `float`, +/// such an attempt will cause `{float.inf}` or `{-float.inf}` to be returned +/// for maximum and minimum respectively. +/// +/// ```example +/// #calc.round(3.1415, digits: 2) +/// #assert(calc.round(3) == 3) +/// #assert(calc.round(3.14) == 3) +/// #assert(calc.round(3.5) == 4.0) +/// #assert(calc.round(3333.45, digits: -2) == 3300.0) +/// #assert(calc.round(-48953.45, digits: -3) == -49000.0) +/// #assert(calc.round(3333, digits: -2) == 3300) +/// #assert(calc.round(-48953, digits: -3) == -49000) +/// #assert(calc.round(decimal("-6.5")) == decimal("-7")) +/// #assert(calc.round(decimal("7.123456789"), digits: 6) == decimal("7.123457")) +/// #assert(calc.round(decimal("3333.45"), digits: -2) == decimal("3300")) +/// #assert(calc.round(decimal("-48953.45"), digits: -3) == decimal("-49000")) +/// ``` +#[func] +pub fn round( + /// The number to round. + value: DecNum, + /// If positive, the number of decimal places. + /// + /// If negative, the number of significant integer digits that should be + /// removed before the decimal point. + #[named] + #[default(0)] + digits: i64, +) -> StrResult<DecNum> { + match value { + DecNum::Int(n) => Ok(DecNum::Int( + round_int_with_precision(n, digits.saturating_as::<i16>()) + .ok_or_else(too_large)?, + )), + DecNum::Float(n) => { + Ok(DecNum::Float(round_with_precision(n, digits.saturating_as::<i16>()))) + } + DecNum::Decimal(n) => Ok(DecNum::Decimal( + n.round(digits.saturating_as::<i32>()).ok_or_else(too_large)?, + )), + } +} + +/// Clamps a number between a minimum and maximum value. +/// +/// ```example +/// #calc.clamp(5, 0, 4) +/// #assert(calc.clamp(5, 0, 10) == 5) +/// #assert(calc.clamp(5, 6, 10) == 6) +/// #assert(calc.clamp(decimal("5.45"), 2, decimal("45.9")) == decimal("5.45")) +/// #assert(calc.clamp(decimal("5.45"), decimal("6.75"), 12) == decimal("6.75")) +/// ``` +#[func] +pub fn clamp( + /// The callsite span. + span: Span, + /// The number to clamp. + value: DecNum, + /// The inclusive minimum value. + min: DecNum, + /// The inclusive maximum value. + max: Spanned<DecNum>, +) -> SourceResult<DecNum> { + // Ignore if there are incompatible types (decimal and float) since that + // will cause `apply3` below to error before calling clamp, avoiding a + // panic. + if min + .apply2(max.v, |min, max| max < min, |min, max| max < min, |min, max| max < min) + .unwrap_or(false) + { + bail!(max.span, "max must be greater than or equal to min") + } + + value + .apply3(min, max.v, i64::clamp, f64::clamp, Decimal::clamp) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span) +} + +/// Determines the minimum of a sequence of values. +/// +/// ```example +/// #calc.min(1, -3, -5, 20, 3, 6) \ +/// #calc.min("typst", "is", "cool") +/// ``` +#[func(title = "Minimum")] +pub fn min( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the minimum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Less) +} + +/// Determines the maximum of a sequence of values. +/// +/// ```example +/// #calc.max(1, -3, -5, 20, 3, 6) \ +/// #calc.max("typst", "is", "cool") +/// ``` +#[func(title = "Maximum")] +pub fn max( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the maximum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax( + span: Span, + values: Vec<Spanned<Value>>, + goal: Ordering, +) -> SourceResult<Value> { + let mut iter = values.into_iter(); + let Some(Spanned { v: mut extremum, .. }) = iter.next() else { + bail!(span, "expected at least one value"); + }; + + for Spanned { v, span } in iter { + let ordering = ops::compare(&v, &extremum).at(span)?; + if ordering == goal { + extremum = v; + } + } + + Ok(extremum) +} + +/// Determines whether an integer is even. +/// +/// ```example +/// #calc.even(4) \ +/// #calc.even(5) \ +/// #range(10).filter(calc.even) +/// ``` +#[func] +pub fn even( + /// The number to check for evenness. + value: i64, +) -> bool { + value % 2 == 0 +} + +/// Determines whether an integer is odd. +/// +/// ```example +/// #calc.odd(4) \ +/// #calc.odd(5) \ +/// #range(10).filter(calc.odd) +/// ``` +#[func] +pub fn odd( + /// The number to check for oddness. + value: i64, +) -> bool { + value % 2 != 0 +} + +/// Calculates the remainder of two numbers. +/// +/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller +/// in magnitude than `y`. +/// +/// This can error if given a [`decimal`] input and the dividend is too small in +/// magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem(7, 3) \ +/// #calc.rem(7, -3) \ +/// #calc.rem(-7, 3) \ +/// #calc.rem(-7, -3) \ +/// #calc.rem(1.75, 0.5) +/// ``` +#[func(title = "Remainder")] +pub fn rem( + /// The span of the function call. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a % b)), + |a, b| Some(DecNum::Float(a % b)), + |a, b| a.checked_rem(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Performs euclidean division of two numbers. +/// +/// The result of this computation is that of a division rounded to the integer +/// `{n}` such that the dividend is greater than or equal to `{n}` times the divisor. +/// +/// ```example +/// #calc.div-euclid(7, 3) \ +/// #calc.div-euclid(7, -3) \ +/// #calc.div-euclid(-7, 3) \ +/// #calc.div-euclid(-7, -3) \ +/// #calc.div-euclid(1.75, 0.5) \ +/// #calc.div-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Division")] +pub fn div_euclid( + /// The callsite span. + span: Span, + /// The dividend of the division. + dividend: DecNum, + /// The divisor of the division. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.div_euclid(b))), + |a, b| Some(DecNum::Float(a.div_euclid(b))), + |a, b| a.checked_div_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span) +} + +/// This calculates the least nonnegative remainder of a division. +/// +/// Warning: Due to a floating point round-off error, the remainder may equal +/// the absolute value of the divisor if the dividend is much smaller in +/// magnitude than the divisor and the dividend is negative. This only applies +/// for floating point inputs. +/// +/// In addition, this can error if given a [`decimal`] input and the dividend is +/// too small in magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem-euclid(7, 3) \ +/// #calc.rem-euclid(7, -3) \ +/// #calc.rem-euclid(-7, 3) \ +/// #calc.rem-euclid(-7, -3) \ +/// #calc.rem-euclid(1.75, 0.5) \ +/// #calc.rem-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Remainder")] +pub fn rem_euclid( + /// The callsite span. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.rem_euclid(b))), + |a, b| Some(DecNum::Float(a.rem_euclid(b))), + |a, b| a.checked_rem_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Calculates the quotient (floored division) of two numbers. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// $ "quo"(a, b) &= floor(a/b) \ +/// "quo"(14, 5) &= #calc.quo(14, 5) \ +/// "quo"(3.46, 0.5) &= #calc.quo(3.46, 0.5) $ +/// ``` +#[func(title = "Quotient")] +pub fn quo( + /// The span of the function call. + span: Span, + /// The dividend of the quotient. + dividend: DecNum, + /// The divisor of the quotient. + divisor: Spanned<DecNum>, +) -> SourceResult<i64> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + let divided = dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a / b)), + |a, b| Some(DecNum::Float(a / b)), + |a, b| a.checked_div(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span)?; + + floor(divided).at(span) +} + +/// A value which can be passed to functions that work with integers and floats. +#[derive(Debug, Copy, Clone)] +pub enum Num { + Int(i64), + Float(f64), +} + +impl Num { + fn float(self) -> f64 { + match self { + Self::Int(v) => v as f64, + Self::Float(v) => v, + } + } +} + +cast! { + Num, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), +} + +/// A value which can be passed to functions that work with integers, floats, +/// and decimals. +#[derive(Debug, Copy, Clone)] +pub enum DecNum { + Int(i64), + Float(f64), + Decimal(Decimal), +} + +impl DecNum { + /// Checks if this number is equivalent to zero. + fn is_zero(self) -> bool { + match self { + Self::Int(i) => i == 0, + Self::Float(f) => f == 0.0, + Self::Decimal(d) => d.is_zero(), + } + } + + /// If this `DecNum` holds an integer or float, returns a float. + /// Otherwise, returns `None`. + fn float(self) -> Option<f64> { + match self { + Self::Int(i) => Some(i as f64), + Self::Float(f) => Some(f), + Self::Decimal(_) => None, + } + } + + /// If this `DecNum` holds an integer or decimal, returns a decimal. + /// Otherwise, returns `None`. + fn decimal(self) -> Option<Decimal> { + match self { + Self::Int(i) => Some(Decimal::from(i)), + Self::Float(_) => None, + Self::Decimal(d) => Some(d), + } + } + + /// Tries to apply a function to two decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply2<T>( + self, + other: Self, + int: impl FnOnce(i64, i64) -> T, + float: impl FnOnce(f64, f64) -> T, + decimal: impl FnOnce(Decimal, Decimal) -> T, + ) -> Option<T> { + match (self, other) { + (Self::Int(a), Self::Int(b)) => Some(int(a, b)), + (Self::Decimal(a), Self::Decimal(b)) => Some(decimal(a, b)), + (Self::Decimal(a), Self::Int(b)) => Some(decimal(a, Decimal::from(b))), + (Self::Int(a), Self::Decimal(b)) => Some(decimal(Decimal::from(a), b)), + (a, b) => Some(float(a.float()?, b.float()?)), + } + } + + /// Tries to apply a function to three decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply3( + self, + other: Self, + third: Self, + int: impl FnOnce(i64, i64, i64) -> i64, + float: impl FnOnce(f64, f64, f64) -> f64, + decimal: impl FnOnce(Decimal, Decimal, Decimal) -> Decimal, + ) -> Option<Self> { + match (self, other, third) { + (Self::Int(a), Self::Int(b), Self::Int(c)) => Some(Self::Int(int(a, b, c))), + (Self::Decimal(a), b, c) => { + Some(Self::Decimal(decimal(a, b.decimal()?, c.decimal()?))) + } + (a, Self::Decimal(b), c) => { + Some(Self::Decimal(decimal(a.decimal()?, b, c.decimal()?))) + } + (a, b, Self::Decimal(c)) => { + Some(Self::Decimal(decimal(a.decimal()?, b.decimal()?, c))) + } + (a, b, c) => Some(Self::Float(float(a.float()?, b.float()?, c.float()?))), + } + } +} + +cast! { + DecNum, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + Self::Decimal(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Decimal => Self::Decimal(v), +} + +/// A value that can be passed to a trigonometric function. +pub enum AngleLike { + Int(i64), + Float(f64), + Angle(Angle), +} + +cast! { + AngleLike, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Angle => Self::Angle(v), +} + +/// The error message when the result is too large to be represented. +#[cold] +fn too_large() -> &'static str { + "the result is too large" +} + +/// The hinted error message when trying to apply an operation to decimal and +/// float operands. +#[cold] +fn cant_apply_to_decimal_and_float() -> HintedString { + HintedString::new("cannot apply this operation to a decimal and a float".into()) + .with_hint( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`", + ) +} diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs new file mode 100644 index 00000000..84f38f36 --- /dev/null +++ b/crates/typst-library/src/foundations/cast.rs @@ -0,0 +1,499 @@ +#[rustfmt::skip] +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + +use std::borrow::Cow; +use std::fmt::Write; +use std::hash::Hash; +use std::ops::Add; + +use ecow::eco_format; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; +use unicode_math_class::MathClass; + +use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; +use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; + +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom<Value>` due to conflicting impls. We could use +/// `From<T> for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe what can be cast into this value. + fn input() -> CastInfo; + + /// Describe what this value can be cast into. + fn output() -> CastInfo; + + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an unacceptable value type. + /// + /// ```ignore + /// assert_eq!( + /// <i64 as Reflect>::error(&Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> HintedString { + Self::input().error(found) + } +} + +impl Reflect for Value { + fn input() -> CastInfo { + CastInfo::Any + } + + fn output() -> CastInfo { + CastInfo::Any + } + + fn castable(_: &Value) -> bool { + true + } +} + +impl<T: Reflect> Reflect for Spanned<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: NativeElement + Reflect> Reflect for Packed<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for StrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for HintedStrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for SourceResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &mut T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl IntoValue for (&Str, &Value) { + fn into_value(self) -> Value { + Value::Array(array![self.0.clone(), self.1.clone()]) + } +} + +impl<T: IntoValue + Clone> IntoValue for Cow<'_, T> { + fn into_value(self) -> Value { + self.into_owned().into_value() + } +} + +impl<T: NativeElement + IntoValue> IntoValue for Packed<T> { + fn into_value(self) -> Value { + Value::Content(self.pack()) + } +} + +impl<T: IntoValue> IntoValue for Spanned<T> { + fn into_value(self) -> Value { + self.v.into_value() + } +} + +/// Cast a Rust type or result into a [`SourceResult<Value>`]. +/// +/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into +/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult<Value>; +} + +impl<T: IntoValue> IntoResult for T { + fn into_result(self, _: Span) -> SourceResult<Value> { + Ok(self.into_value()) + } +} + +impl<T: IntoValue> IntoResult for StrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) + } +} + +impl<T: IntoValue> IntoResult for HintedStrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) + } +} + +impl<T: IntoValue> IntoResult for SourceResult<T> { + fn into_result(self, _: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value) + } +} + +impl<T: IntoValue> IntoValue for fn() -> T { + fn into_value(self) -> Value { + self().into_value() + } +} + +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue<V = Value>: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> HintedStrResult<Self>; +} + +impl FromValue for Value { + fn from_value(value: Value) -> HintedStrResult<Self> { + Ok(value) + } +} + +impl<T: NativeElement + FromValue> FromValue for Packed<T> { + fn from_value(mut value: Value) -> HintedStrResult<Self> { + if let Value::Content(content) = value { + match content.into_packed::<T>() { + Ok(packed) => return Ok(packed), + Err(content) => value = Value::Content(content), + } + } + let val = T::from_value(value)?; + Ok(Packed::new(val)) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> HintedStrResult<Self> { + T::from_value(value.v) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> HintedStrResult<Self> { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } +} + +/// Describes a possible value for a cast. +#[derive(Debug, Clone, PartialEq, Hash, PartialOrd)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(Type), + /// Multiple alternatives. + Union(Vec<Self>), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> HintedString { + let mut matching_type = false; + let mut parts = vec![]; + + self.walk(|info| match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr()); + if value.ty() == found.ty() { + matching_type = true; + } + } + CastInfo::Type(ty) => parts.push(eco_format!("{ty}")), + CastInfo::Union(_) => {} + }); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + msg.push_str(&repr::separated_list(&parts, "or")); + + if !matching_type { + msg.push_str(", found "); + write!(msg, "{}", found.ty()).unwrap(); + } + + let mut msg: HintedString = msg.into(); + + if let Value::Int(i) = found { + if !matching_type && parts.iter().any(|p| p == "length") { + msg.hint(eco_format!("a length needs a unit - did you mean {i}pt?")); + } + } else if let Value::Str(s) = found { + if !matching_type && parts.iter().any(|p| p == "label") { + if typst_syntax::is_valid_label_literal_id(s) { + msg.hint(eco_format!( + "use `<{s}>` or `label({})` to create a label", + s.repr() + )); + } else { + msg.hint(eco_format!("use `label({})` to create a label", s.repr())); + } + } + } else if let Value::Decimal(_) = found { + if !matching_type && parts.iter().any(|p| p == "float") { + msg.hint(eco_format!( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`" + )); + } + } + + msg + } + + /// Walk all contained non-union infos. + pub fn walk<F>(&self, mut f: F) + where + F: FnMut(&Self), + { + fn inner<F>(info: &CastInfo, f: &mut F) + where + F: FnMut(&CastInfo), + { + if let CastInfo::Union(infos) = info { + for child in infos { + inner(child, f); + } + } else { + f(info); + } + } + + inner(self, &mut f) + } +} + +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + for cast in rhs { + if !lhs.contains(&cast) { + lhs.push(cast); + } + } + lhs + } + (Self::Union(mut lhs), rhs) => { + if !lhs.contains(&rhs) { + lhs.push(rhs); + } + lhs + } + (lhs, Self::Union(mut rhs)) => { + if !rhs.contains(&lhs) { + rhs.insert(0, lhs); + } + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} + +/// A container for an argument. +pub trait Container { + /// The contained type. + type Inner; +} + +impl<T> Container for Option<T> { + type Inner = T; +} + +impl<T> Container for Vec<T> { + type Inner = T; +} + +impl<T, const N: usize> Container for SmallVec<[T; N]> { + type Inner = T; +} + +/// An uninhabitable type. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Never {} + +impl Reflect for Never { + fn input() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn output() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { + false + } +} + +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} + } +} + +impl FromValue for Never { + fn from_value(value: Value) -> HintedStrResult<Self> { + Err(Self::error(&value)) + } +} + +cast! { + MathClass, + self => IntoValue::into_value(match self { + MathClass::Normal => "normal", + MathClass::Alphabetic => "alphabetic", + MathClass::Binary => "binary", + MathClass::Closing => "closing", + MathClass::Diacritic => "diacritic", + MathClass::Fence => "fence", + MathClass::GlyphPart => "glyph-part", + MathClass::Large => "large", + MathClass::Opening => "opening", + MathClass::Punctuation => "punctuation", + MathClass::Relation => "relation", + MathClass::Space => "space", + MathClass::Unary => "unary", + MathClass::Vary => "vary", + MathClass::Special => "special", + }), + /// The default class for non-special things. + "normal" => MathClass::Normal, + /// Punctuation, e.g. a comma. + "punctuation" => MathClass::Punctuation, + /// An opening delimiter, e.g. `(`. + "opening" => MathClass::Opening, + /// A closing delimiter, e.g. `)`. + "closing" => MathClass::Closing, + /// A delimiter that is the same on both sides, e.g. `|`. + "fence" => MathClass::Fence, + /// A large operator like `sum`. + "large" => MathClass::Large, + /// A relation like `=` or `prec`. + "relation" => MathClass::Relation, + /// A unary operator like `not`. + "unary" => MathClass::Unary, + /// A binary operator like `times`. + "binary" => MathClass::Binary, + /// An operator that can be both unary or binary like `+`. + "vary" => MathClass::Vary, +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs new file mode 100644 index 00000000..a274b8bf --- /dev/null +++ b/crates/typst-library/src/foundations/content.rs @@ -0,0 +1,1007 @@ +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::iter::{self, Sum}; +use std::marker::PhantomData; +use std::ops::{Add, AddAssign, Deref, DerefMut}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use smallvec::smallvec; +use typst_syntax::Span; +use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; + +use crate::diag::{SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, + NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, + Value, +}; +use crate::introspection::Location; +use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; +use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; +use crate::text::UnderlineElem; + +/// A piece of document content. +/// +/// This type is at the heart of Typst. All markup you write and most +/// [functions]($function) you call produce content values. You can create a +/// content value by enclosing markup in square brackets. This is also how you +/// pass content to functions. +/// +/// # Example +/// ```example +/// Type of *Hello!* is +/// #type([*Hello!*]) +/// ``` +/// +/// Content can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. Wherever +/// content is expected, you can also pass a [string]($str) or `{none}`. +/// +/// # Representation +/// Content consists of elements with fields. When constructing an element with +/// its _element function,_ you provide these fields as arguments and when you +/// have a content value, you can access its fields with [field access +/// syntax]($scripting/#field-access). +/// +/// Some fields are required: These must be provided when constructing an +/// element and as a consequence, they are always available through field access +/// on content of that type. Required fields are marked as such in the +/// documentation. +/// +/// Most fields are optional: Like required fields, they can be passed to the +/// element function to configure them for a single element. However, these can +/// also be configured with [set rules]($styling/#set-rules) to apply them to +/// all elements within a scope. Optional fields are only available with field +/// access syntax when they were explicitly passed to the element function, not +/// when they result from a set rule. +/// +/// Each element has a default appearance. However, you can also completely +/// customize its appearance with a [show rule]($styling/#show-rules). The show +/// rule is passed the element. It can access the element's field and produce +/// arbitrary content from it. +/// +/// In the web app, you can hover over a content variable to see exactly which +/// elements the content is composed of and what fields they have. +/// Alternatively, you can inspect the output of the [`repr`] function. +#[ty(scope, cast)] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Content { + /// The partially element-dependent inner data. + inner: Arc<Inner<dyn Bounds>>, + /// The element's source code location. + span: Span, +} + +/// The inner representation behind the `Arc`. +#[derive(Hash)] +struct Inner<T: ?Sized + 'static> { + /// An optional label attached to the element. + label: Option<Label>, + /// The element's location which identifies it in the layouted output. + location: Option<Location>, + /// Manages the element during realization. + /// - If bit 0 is set, the element is prepared. + /// - If bit n is set, the element is guarded against the n-th show rule + /// recipe from the top of the style chain (counting from 1). + lifecycle: SmallBitSet, + /// The element's raw data. + elem: LazyHash<T>, +} + +impl Content { + /// Creates a new content from an element. + pub fn new<T: NativeElement>(elem: T) -> Self { + Self { + inner: Arc::new(Inner { + label: None, + location: None, + lifecycle: SmallBitSet::new(), + elem: elem.into(), + }), + span: Span::detached(), + } + } + + /// Creates a empty sequence content. + pub fn empty() -> Self { + singleton!(Content, SequenceElem::default().pack()).clone() + } + + /// Get the element of this content. + pub fn elem(&self) -> Element { + self.inner.elem.dyn_elem() + } + + /// Get the span of the content. + pub fn span(&self) -> Span { + self.span + } + + /// Set the span of the content. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Get the label of the content. + pub fn label(&self) -> Option<Label> { + self.inner.label + } + + /// Attach a label to the content. + pub fn labelled(mut self, label: Label) -> Self { + self.set_label(label); + self + } + + /// Set the label of the content. + pub fn set_label(&mut self, label: Label) { + self.make_mut().label = Some(label); + } + + /// Assigns a location to the content. + /// + /// This identifies the content and e.g. makes it linkable by + /// `.linked(Destination::Location(loc))`. + /// + /// Useful in combination with [`Location::variant`]. + pub fn located(mut self, loc: Location) -> Self { + self.set_location(loc); + self + } + + /// Set the location of the content. + pub fn set_location(&mut self, location: Location) { + self.make_mut().location = Some(location); + } + + /// Check whether a show rule recipe is disabled. + pub fn is_guarded(&self, index: RecipeIndex) -> bool { + self.inner.lifecycle.contains(index.0) + } + + /// Disable a show rule recipe. + pub fn guarded(mut self, index: RecipeIndex) -> Self { + self.make_mut().lifecycle.insert(index.0); + self + } + + /// Whether this content has already been prepared. + pub fn is_prepared(&self) -> bool { + self.inner.lifecycle.contains(0) + } + + /// Mark this content as prepared. + pub fn mark_prepared(&mut self) { + self.make_mut().lifecycle.insert(0); + } + + /// Get a field by ID. + /// + /// This is the preferred way to access fields. However, you can only use it + /// if you have set the field IDs yourself or are using the field IDs + /// generated by the `#[elem]` macro. + pub fn get( + &self, + id: u8, + styles: Option<StyleChain>, + ) -> Result<Value, FieldAccessError> { + if id == 255 { + if let Some(label) = self.label() { + return Ok(label.into_value()); + } + } + match styles { + Some(styles) => self.inner.elem.field_with_styles(id, styles), + None => self.inner.elem.field(id), + } + } + + /// Get a field by name. + /// + /// If you have access to the field IDs of the element, use [`Self::get`] + /// instead. + pub fn get_by_name(&self, name: &str) -> Result<Value, FieldAccessError> { + if name == "label" { + if let Some(label) = self.label() { + return Ok(label.into_value()); + } + } + let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?; + self.get(id, None) + } + + /// Get a field by ID, returning a missing field error if it does not exist. + /// + /// This is the preferred way to access fields. However, you can only use it + /// if you have set the field IDs yourself or are using the field IDs + /// generated by the `#[elem]` macro. + pub fn field(&self, id: u8) -> StrResult<Value> { + self.get(id, None) + .map_err(|e| e.message(self, self.elem().field_name(id).unwrap())) + } + + /// Get a field by name, returning a missing field error if it does not + /// exist. + /// + /// If you have access to the field IDs of the element, use [`Self::field`] + /// instead. + pub fn field_by_name(&self, name: &str) -> StrResult<Value> { + self.get_by_name(name).map_err(|e| e.message(self, name)) + } + + /// Resolve all fields with the styles and save them in-place. + pub fn materialize(&mut self, styles: StyleChain) { + self.make_mut().elem.materialize(styles); + } + + /// Create a new sequence element from multiples elements. + pub fn sequence(iter: impl IntoIterator<Item = Self>) -> Self { + let vec: Vec<_> = iter.into_iter().collect(); + if vec.is_empty() { + Self::empty() + } else if vec.len() == 1 { + vec.into_iter().next().unwrap() + } else { + SequenceElem::new(vec).into() + } + } + + /// Whether the contained element is of type `T`. + pub fn is<T: NativeElement>(&self) -> bool { + self.inner.elem.dyn_type_id() == TypeId::of::<T>() + } + + /// Downcasts the element to a packed value. + pub fn to_packed<T: NativeElement>(&self) -> Option<&Packed<T>> { + Packed::from_ref(self) + } + + /// Downcasts the element to a mutable packed value. + pub fn to_packed_mut<T: NativeElement>(&mut self) -> Option<&mut Packed<T>> { + Packed::from_mut(self) + } + + /// Downcasts the element into an owned packed value. + pub fn into_packed<T: NativeElement>(self) -> Result<Packed<T>, Self> { + Packed::from_owned(self) + } + + /// Extract the raw underlying element. + pub fn unpack<T: NativeElement>(self) -> Result<T, Self> { + self.into_packed::<T>().map(Packed::unpack) + } + + /// Makes sure the content is not shared and returns a mutable reference to + /// the inner data. + fn make_mut(&mut self) -> &mut Inner<dyn Bounds> { + let arc = &mut self.inner; + if Arc::strong_count(arc) > 1 || Arc::weak_count(arc) > 0 { + *self = arc.elem.dyn_clone(arc, self.span); + } + Arc::get_mut(&mut self.inner).unwrap() + } + + /// Whether the contained element has the given capability. + pub fn can<C>(&self) -> bool + where + C: ?Sized + 'static, + { + self.elem().can::<C>() + } + + /// Cast to a trait object if the contained element has the given + /// capability. + pub fn with<C>(&self) -> Option<&C> + where + C: ?Sized + 'static, + { + // Safety: The vtable comes from the `Capable` implementation which + // guarantees to return a matching vtable for `Packed<T>` and `C`. + // Since any `Packed<T>` is a repr(transparent) `Content`, we can also + // use a `*const Content` pointer. + let vtable = self.elem().vtable()(TypeId::of::<C>())?; + let data = self as *const Content as *const (); + Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) }) + } + + /// Cast to a mutable trait object if the contained element has the given + /// capability. + pub fn with_mut<C>(&mut self) -> Option<&mut C> + where + C: ?Sized + 'static, + { + // Safety: The vtable comes from the `Capable` implementation which + // guarantees to return a matching vtable for `Packed<T>` and `C`. + // Since any `Packed<T>` is a repr(transparent) `Content`, we can also + // use a `*const Content` pointer. + // + // The resulting trait object contains an `&mut Packed<T>`. We do _not_ + // need to ensure that we hold the only reference to the `Arc` here + // because `Packed<T>`'s DerefMut impl will take care of that if + // mutable access is required. + let vtable = self.elem().vtable()(TypeId::of::<C>())?; + let data = self as *mut Content as *mut (); + Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) }) + } + + /// Whether the content is an empty sequence. + pub fn is_empty(&self) -> bool { + let Some(sequence) = self.to_packed::<SequenceElem>() else { + return false; + }; + + sequence.children.is_empty() + } + + /// Also auto expands sequence of sequences into flat sequence + pub fn sequence_recursive_for_each<'a>(&'a self, f: &mut impl FnMut(&'a Self)) { + if let Some(sequence) = self.to_packed::<SequenceElem>() { + for child in &sequence.children { + child.sequence_recursive_for_each(f); + } + } else { + f(self); + } + } + + /// Style this content with a recipe, eagerly applying it if possible. + pub fn styled_with_recipe( + self, + engine: &mut Engine, + context: Tracked<Context>, + recipe: Recipe, + ) -> SourceResult<Self> { + if recipe.selector().is_none() { + recipe.apply(engine, context, self) + } else { + Ok(self.styled(recipe)) + } + } + + /// Repeat this content `count` times. + pub fn repeat(&self, count: usize) -> Self { + Self::sequence(std::iter::repeat_with(|| self.clone()).take(count)) + } + + /// Style this content with a style entry. + pub fn styled(mut self, style: impl Into<Style>) -> Self { + if let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.apply_one(style.into()); + self + } else { + self.styled_with_map(style.into().into()) + } + } + + /// Style this content with a full style map. + pub fn styled_with_map(mut self, styles: Styles) -> Self { + if styles.is_empty() { + return self; + } + + if let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.apply(styles); + self + } else { + StyledElem::new(self, styles).into() + } + } + + /// Style this content with a full style map in-place. + pub fn style_in_place(&mut self, styles: Styles) { + if styles.is_empty() { + return; + } + + if let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.apply(styles); + } else { + *self = StyledElem::new(std::mem::take(self), styles).into(); + } + } + + /// Queries the content tree for all elements that match the given selector. + /// + /// Elements produced in `show` rules will not be included in the results. + pub fn query(&self, selector: Selector) -> Vec<Content> { + let mut results = Vec::new(); + self.traverse(&mut |element| { + if selector.matches(&element, None) { + results.push(element); + } + }); + results + } + + /// Queries the content tree for the first element that match the given + /// selector. + /// + /// Elements produced in `show` rules will not be included in the results. + pub fn query_first(&self, selector: Selector) -> Option<Content> { + let mut result = None; + self.traverse(&mut |element| { + if result.is_none() && selector.matches(&element, None) { + result = Some(element); + } + }); + result + } + + /// Extracts the plain text of this content. + pub fn plain_text(&self) -> EcoString { + let mut text = EcoString::new(); + self.traverse(&mut |element| { + if let Some(textable) = element.with::<dyn PlainText>() { + textable.plain_text(&mut text); + } + }); + text + } + + /// Traverse this content. + fn traverse<F>(&self, f: &mut F) + where + F: FnMut(Content), + { + f(self.clone()); + + self.inner + .elem + .fields() + .into_iter() + .for_each(|(_, value)| walk_value(value, f)); + + /// Walks a given value to find any content that matches the selector. + fn walk_value<F>(value: Value, f: &mut F) + where + F: FnMut(Content), + { + match value { + Value::Content(content) => content.traverse(f), + Value::Array(array) => { + for value in array { + walk_value(value, f); + } + } + _ => {} + } + } + } +} + +impl Content { + /// Strongly emphasize this content. + pub fn strong(self) -> Self { + StrongElem::new(self).pack() + } + + /// Emphasize this content. + pub fn emph(self) -> Self { + EmphElem::new(self).pack() + } + + /// Underline this content. + pub fn underlined(self) -> Self { + UnderlineElem::new(self).pack() + } + + /// Link the content somewhere. + pub fn linked(self, dest: Destination) -> Self { + self.styled(LinkElem::set_dests(smallvec![dest])) + } + + /// Set alignments for this content. + pub fn aligned(self, align: Alignment) -> Self { + self.styled(AlignElem::set_alignment(align)) + } + + /// Pad this content at the sides. + pub fn padded(self, padding: Sides<Rel<Length>>) -> Self { + PadElem::new(self) + .with_left(padding.left) + .with_top(padding.top) + .with_right(padding.right) + .with_bottom(padding.bottom) + .pack() + } + + /// Transform this content's contents without affecting layout. + pub fn moved(self, delta: Axes<Rel<Length>>) -> Self { + MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() + } +} + +#[scope] +impl Content { + /// The content's element function. This function can be used to create the element + /// contained in this content. It can be used in set and show rules for the + /// element. Can be compared with global functions to check whether you have + /// a specific + /// kind of element. + #[func] + pub fn func(&self) -> Element { + self.elem() + } + + /// Whether the content has the specified field. + #[func] + pub fn has( + &self, + /// The field to look for. + field: Str, + ) -> bool { + if field.as_str() == "label" { + return self.label().is_some(); + } + + let Some(id) = self.elem().field_id(&field) else { + return false; + }; + + self.inner.elem.has(id) + } + + /// Access the specified field on the content. Returns the default value if + /// the field does not exist or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The field to access. + field: Str, + /// A default value to return if the field does not exist. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.get_by_name(&field) + .or_else(|e| default.ok_or(e)) + .map_err(|e| e.message_no_default(self, &field)) + } + + /// Returns the fields of this content. + /// + /// ```example + /// #rect( + /// width: 10cm, + /// height: 10cm, + /// ).fields() + /// ``` + #[func] + pub fn fields(&self) -> Dict { + let mut dict = self.inner.elem.fields(); + if let Some(label) = self.label() { + dict.insert("label".into(), label.into_value()); + } + dict + } + + /// The location of the content. This is only available on content returned + /// by [query] or provided by a [show rule]($reference/styling/#show-rules), + /// for other content it will be `{none}`. The resulting location can be + /// used with [counters]($counter), [state] and [queries]($query). + #[func] + pub fn location(&self) -> Option<Location> { + self.inner.location + } +} + +impl Default for Content { + fn default() -> Self { + Self::empty() + } +} + +impl Debug for Content { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.inner.elem.fmt(f) + } +} + +impl<T: NativeElement> From<T> for Content { + fn from(value: T) -> Self { + Self::new(value) + } +} + +impl PartialEq for Content { + fn eq(&self, other: &Self) -> bool { + // Additional short circuit for different elements. + self.elem() == other.elem() && self.inner.elem.dyn_eq(other) + } +} + +impl Repr for Content { + fn repr(&self) -> EcoString { + self.inner.elem.repr() + } +} + +impl Add for Content { + type Output = Self; + + fn add(self, mut rhs: Self) -> Self::Output { + let mut lhs = self; + match (lhs.to_packed_mut::<SequenceElem>(), rhs.to_packed_mut::<SequenceElem>()) { + (Some(seq_lhs), Some(rhs)) => { + seq_lhs.children.extend(rhs.children.iter().cloned()); + lhs + } + (Some(seq_lhs), None) => { + seq_lhs.children.push(rhs); + lhs + } + (None, Some(rhs_seq)) => { + rhs_seq.children.insert(0, lhs); + rhs + } + (None, None) => Self::sequence([lhs, rhs]), + } + } +} + +impl<'a> Add<&'a Self> for Content { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + let mut lhs = self; + match (lhs.to_packed_mut::<SequenceElem>(), rhs.to_packed::<SequenceElem>()) { + (Some(seq_lhs), Some(rhs)) => { + seq_lhs.children.extend(rhs.children.iter().cloned()); + lhs + } + (Some(seq_lhs), None) => { + seq_lhs.children.push(rhs.clone()); + lhs + } + (None, Some(_)) => { + let mut rhs = rhs.clone(); + rhs.to_packed_mut::<SequenceElem>().unwrap().children.insert(0, lhs); + rhs + } + (None, None) => Self::sequence([lhs, rhs.clone()]), + } + } +} + +impl AddAssign for Content { + fn add_assign(&mut self, rhs: Self) { + *self = std::mem::take(self) + rhs; + } +} + +impl AddAssign<&Self> for Content { + fn add_assign(&mut self, rhs: &Self) { + *self = std::mem::take(self) + rhs; + } +} + +impl Sum for Content { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self::sequence(iter) + } +} + +impl Serialize for Content { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.collect_map( + iter::once(("func".into(), self.func().name().into_value())) + .chain(self.fields()), + ) + } +} + +/// The trait that combines all the other traits into a trait object. +trait Bounds: Debug + Repr + Fields + Send + Sync + 'static { + fn dyn_type_id(&self) -> TypeId; + fn dyn_elem(&self) -> Element; + fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content; + fn dyn_hash(&self, hasher: &mut dyn Hasher); + fn dyn_eq(&self, other: &Content) -> bool; +} + +impl<T: NativeElement> Bounds for T { + fn dyn_type_id(&self) -> TypeId { + TypeId::of::<Self>() + } + + fn dyn_elem(&self) -> Element { + Self::elem() + } + + fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content { + Content { + inner: Arc::new(Inner { + label: inner.label, + location: inner.location, + lifecycle: inner.lifecycle.clone(), + elem: LazyHash::reuse(self.clone(), &inner.elem), + }), + span, + } + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } + + fn dyn_eq(&self, other: &Content) -> bool { + let Some(other) = other.to_packed::<Self>() else { + return false; + }; + *self == **other + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// A packed element of a static type. +#[derive(Clone, PartialEq, Hash)] +#[repr(transparent)] +pub struct Packed<T: NativeElement>( + /// Invariant: Must be of type `T`. + Content, + PhantomData<T>, +); + +impl<T: NativeElement> Packed<T> { + /// Pack element while retaining its static type. + pub fn new(element: T) -> Self { + // Safety: The element is known to be of type `T`. + Packed(element.pack(), PhantomData) + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_ref(content: &Content) -> Option<&Self> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) }); + } + None + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_mut(content: &mut Content) -> Option<&mut Self> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Some(unsafe { + std::mem::transmute::<&mut Content, &mut Packed<T>>(content) + }); + } + None + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_owned(content: Content) -> Result<Self, Content> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) }); + } + Err(content) + } + + /// Pack back into content. + pub fn pack(self) -> Content { + self.0 + } + + /// Extract the raw underlying element. + pub fn unpack(self) -> T { + // This function doesn't yet need owned self, but might in the future. + (*self).clone() + } + + /// The element's span. + pub fn span(&self) -> Span { + self.0.span() + } + + /// Set the span of the element. + pub fn spanned(self, span: Span) -> Self { + Self(self.0.spanned(span), PhantomData) + } + + /// Accesses the label of the element. + pub fn label(&self) -> Option<Label> { + self.0.label() + } + + /// Accesses the location of the element. + pub fn location(&self) -> Option<Location> { + self.0.location() + } + + /// Sets the location of the element. + pub fn set_location(&mut self, location: Location) { + self.0.set_location(location); + } +} + +impl<T: NativeElement> AsRef<T> for Packed<T> { + fn as_ref(&self) -> &T { + self + } +} + +impl<T: NativeElement> AsMut<T> for Packed<T> { + fn as_mut(&mut self) -> &mut T { + self + } +} + +impl<T: NativeElement> Deref for Packed<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + // Safety: + // - Packed<T> guarantees that the content trait object wraps + // an element of type `T`. + // - This downcast works the same way as dyn Any's does. We can't reuse + // that one because we don't want to pay the cost for every deref. + let elem = &*self.0.inner.elem; + unsafe { &*(elem as *const dyn Bounds as *const T) } + } +} + +impl<T: NativeElement> DerefMut for Packed<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + // Safety: + // - Packed<T> guarantees that the content trait object wraps + // an element of type `T`. + // - We have guaranteed unique access thanks to `make_mut`. + // - This downcast works the same way as dyn Any's does. We can't reuse + // that one because we don't want to pay the cost for every deref. + let elem = &mut *self.0.make_mut().elem; + unsafe { &mut *(elem as *mut dyn Bounds as *mut T) } + } +} + +impl<T: NativeElement + Debug> Debug for Packed<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// A sequence of content. +#[elem(Debug, Repr, PartialEq)] +pub struct SequenceElem { + /// The elements. + #[required] + pub children: Vec<Content>, +} + +impl Debug for SequenceElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Sequence ")?; + f.debug_list().entries(&self.children).finish() + } +} + +// Derive is currently incompatible with `elem` macro. +#[allow(clippy::derivable_impls)] +impl Default for SequenceElem { + fn default() -> Self { + Self { children: Default::default() } + } +} + +impl PartialEq for SequenceElem { + fn eq(&self, other: &Self) -> bool { + self.children.iter().eq(other.children.iter()) + } +} + +impl Repr for SequenceElem { + fn repr(&self) -> EcoString { + if self.children.is_empty() { + "[]".into() + } else { + let elements = crate::foundations::repr::pretty_array_like( + &self.children.iter().map(|c| c.inner.elem.repr()).collect::<Vec<_>>(), + false, + ); + eco_format!("sequence{}", elements) + } + } +} + +/// Content alongside styles. +#[elem(Debug, Repr, PartialEq)] +pub struct StyledElem { + /// The content. + #[required] + pub child: Content, + /// The styles. + #[required] + pub styles: Styles, +} + +impl Debug for StyledElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + for style in self.styles.iter() { + writeln!(f, "#{style:?}")?; + } + self.child.fmt(f) + } +} + +impl PartialEq for StyledElem { + fn eq(&self, other: &Self) -> bool { + self.child == other.child + } +} + +impl Repr for StyledElem { + fn repr(&self) -> EcoString { + eco_format!("styled(child: {}, ..)", self.child.repr()) + } +} + +/// Tries to extract the plain-text representation of the element. +pub trait PlainText { + /// Write this element's plain text into the given buffer. + fn plain_text(&self, text: &mut EcoString); +} + +/// An error arising when trying to access a field of content. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FieldAccessError { + Unknown, + Unset, + Internal, +} + +impl FieldAccessError { + /// Formats the error message given the content and the field name. + #[cold] + pub fn message(self, content: &Content, field: &str) -> EcoString { + let elem_name = content.elem().name(); + match self { + FieldAccessError::Unknown => { + eco_format!("{elem_name} does not have field {}", field.repr()) + } + FieldAccessError::Unset => { + eco_format!( + "field {} in {elem_name} is not known at this point", + field.repr() + ) + } + FieldAccessError::Internal => { + eco_format!( + "internal error when accessing field {} in {elem_name} – this is a bug", + field.repr() + ) + } + } + } + + /// Formats the error message for an `at` calls without a default value. + #[cold] + pub fn message_no_default(self, content: &Content, field: &str) -> EcoString { + let mut msg = self.message(content, field); + msg.push_str(" and no default was specified"); + msg + } +} diff --git a/crates/typst-library/src/foundations/context.rs b/crates/typst-library/src/foundations/context.rs new file mode 100644 index 00000000..bf4bdcd2 --- /dev/null +++ b/crates/typst-library/src/foundations/context.rs @@ -0,0 +1,85 @@ +use comemo::Track; + +use crate::diag::{bail, Hint, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value, +}; +use crate::introspection::{Locatable, Location}; + +/// Data that is contextually made available to code. +/// +/// _Contextual_ functions and expressions require the presence of certain +/// pieces of context to be evaluated. This includes things like `text.lang`, +/// `measure`, or `counter(heading).get()`. +#[derive(Debug, Default, Clone, Hash)] +pub struct Context<'a> { + /// The location in the document. + pub location: Option<Location>, + /// The active styles. + pub styles: Option<StyleChain<'a>>, +} + +impl<'a> Context<'a> { + /// An empty context. + pub fn none() -> Self { + Self::default() + } + + /// Create a new context from its parts. + pub fn new(location: Option<Location>, styles: Option<StyleChain<'a>>) -> Self { + Self { location, styles } + } +} + +#[comemo::track] +impl<'a> Context<'a> { + /// Try to extract the location. + pub fn location(&self) -> HintedStrResult<Location> { + require(self.location) + } + + /// Try to extract the styles. + pub fn styles(&self) -> HintedStrResult<StyleChain<'a>> { + require(self.styles) + } + + /// Guard access to the introspector by requiring at least some piece of context. + pub fn introspect(&self) -> HintedStrResult<()> { + require(self.location.map(|_| ()).or(self.styles.map(|_| ()))) + } +} + +/// Extracts an optional piece of context, yielding an error with hints if +/// it isn't available. +fn require<T>(val: Option<T>) -> HintedStrResult<T> { + val.ok_or("can only be used when context is known") + .hint("try wrapping this in a `context` expression") + .hint( + "the `context` expression should wrap everything that depends on this function", + ) +} + +/// Executes a `context` block. +#[elem(Construct, Locatable, Show)] +pub struct ContextElem { + /// The function to call with the context. + #[required] + #[internal] + func: Func, +} + +impl Construct for ContextElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<ContextElem> { + #[typst_macros::time(name = "context", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let loc = self.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + Ok(self.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) + } +} diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs new file mode 100644 index 00000000..d15cd417 --- /dev/null +++ b/crates/typst-library/src/foundations/datetime.rs @@ -0,0 +1,580 @@ +use std::cmp::Ordering; +use std::hash::Hash; +use std::ops::{Add, Sub}; + +use ecow::{eco_format, EcoString, EcoVec}; +use time::error::{Format, InvalidFormatDescription}; +use time::macros::format_description; +use time::{format_description, Month, PrimitiveDateTime}; + +use crate::diag::{bail, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value, +}; +use crate::World; + +/// Represents a date, a time, or a combination of both. +/// +/// Can be created by either specifying a custom datetime using this type's +/// constructor function or getting the current date with +/// [`datetime.today`]($datetime.today). +/// +/// # Example +/// ```example +/// #let date = datetime( +/// year: 2020, +/// month: 10, +/// day: 4, +/// ) +/// +/// #date.display() \ +/// #date.display( +/// "y:[year repr:last_two]" +/// ) +/// +/// #let time = datetime( +/// hour: 18, +/// minute: 2, +/// second: 23, +/// ) +/// +/// #time.display() \ +/// #time.display( +/// "h:[hour repr:12][period]" +/// ) +/// ``` +/// +/// # Datetime and Duration +/// You can get a [duration] by subtracting two datetime: +/// ```example +/// #let first-of-march = datetime(day: 1, month: 3, year: 2024) +/// #let first-of-jan = datetime(day: 1, month: 1, year: 2024) +/// #let distance = first-of-march - first-of-jan +/// #distance.hours() +/// ``` +/// +/// You can also add/subtract a datetime and a duration to retrieve a new, +/// offset datetime: +/// ```example +/// #let date = datetime(day: 1, month: 3, year: 2024) +/// #let two-days = duration(days: 2) +/// #let two-days-earlier = date - two-days +/// #let two-days-later = date + two-days +/// +/// #date.display() \ +/// #two-days-earlier.display() \ +/// #two-days-later.display() +/// ``` +/// +/// # Format +/// You can specify a customized formatting using the +/// [`display`]($datetime.display) method. The format of a datetime is +/// specified by providing _components_ with a specified number of _modifiers_. +/// A component represents a certain part of the datetime that you want to +/// display, and with the help of modifiers you can define how you want to +/// display that component. In order to display a component, you wrap the name +/// of the component in square brackets (e.g. `[[year]]` will display the year). +/// In order to add modifiers, you add a space after the component name followed +/// by the name of the modifier, a colon and the value of the modifier (e.g. +/// `[[month repr:short]]` will display the short representation of the month). +/// +/// The possible combination of components and their respective modifiers is as +/// follows: +/// +/// - `year`: Displays the year of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// year is padded. +/// - `repr` Can be either `full` in which case the full year is displayed or +/// `last_two` in which case only the last two digits are displayed. +/// - `sign`: Can be either `automatic` or `mandatory`. Specifies when the +/// sign should be displayed. +/// - `month`: Displays the month of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// month is padded. +/// - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the +/// month should be displayed as a number or a word. Unfortunately, when +/// choosing the word representation, it can currently only display the +/// English version. In the future, it is planned to support localization. +/// - `day`: Displays the day of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// day is padded. +/// - `week_number`: Displays the week number of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// week number is padded. +/// - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`, +/// week numbers are between 1 and 53, while the other ones are between 0 +/// and 53. +/// - `weekday`: Displays the weekday of the date. +/// - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case +/// of `long` and `short`, the corresponding English name will be displayed +/// (same as for the month, other languages are currently not supported). In +/// the case of `sunday` and `monday`, the numerical value will be displayed +/// (assuming Sunday and Monday as the first day of the week, respectively). +/// - `one_indexed`: Can be either `true` or `false`. Defines whether the +/// numerical representation of the week starts with 0 or 1. +/// - `hour`: Displays the hour of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// hour is padded. +/// - `repr`: Can be either `24` or `12`. Changes whether the hour is +/// displayed in the 24-hour or 12-hour format. +/// - `period`: The AM/PM part of the hour +/// - `case`: Can be `lower` to display it in lower case and `upper` to +/// display it in upper case. +/// - `minute`: Displays the minute of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// minute is padded. +/// - `second`: Displays the second of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// second is padded. +/// +/// Keep in mind that not always all components can be used. For example, if you +/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it +/// will be stored as a plain date internally, meaning that you cannot use +/// components such as `hour` or `minute`, which would only work on datetimes +/// that have a specified time. +#[ty(scope, cast)] +#[derive(Debug, Clone, Copy, PartialEq, Hash)] +pub enum Datetime { + /// Representation as a date. + Date(time::Date), + /// Representation as a time. + Time(time::Time), + /// Representation as a combination of date and time. + Datetime(time::PrimitiveDateTime), +} + +impl Datetime { + /// Create a datetime from year, month, and day. + pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> { + Some(Datetime::Date( + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?, + )) + } + + /// Create a datetime from hour, minute, and second. + pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> { + Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?)) + } + + /// Create a datetime from day and time. + pub fn from_ymd_hms( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Option<Self> { + let date = + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?; + let time = time::Time::from_hms(hour, minute, second).ok()?; + Some(Datetime::Datetime(PrimitiveDateTime::new(date, time))) + } + + /// Try to parse a dictionary as a TOML date. + pub fn from_toml_dict(dict: &Dict) -> Option<Self> { + if dict.len() != 1 { + return None; + } + + let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else { + return None; + }; + + if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = + time::Date::parse(string, &format_description!("[year]-[month]-[day]")) + { + Self::from_ymd(d.year(), d.month() as u8, d.day()) + } else if let Ok(d) = + time::Time::parse(string, &format_description!("[hour]:[minute]:[second]")) + { + Self::from_hms(d.hour(), d.minute(), d.second()) + } else { + None + } + } + + /// Which kind of variant this datetime stores. + pub fn kind(&self) -> &'static str { + match self { + Datetime::Datetime(_) => "datetime", + Datetime::Date(_) => "date", + Datetime::Time(_) => "time", + } + } +} + +#[scope] +impl Datetime { + /// Creates a new datetime. + /// + /// You can specify the [datetime] using a year, month, day, hour, minute, + /// and second. + /// + /// _Note_: Depending on which components of the datetime you specify, Typst + /// will store it in one of the following three ways: + /// * If you specify year, month and day, Typst will store just a date. + /// * If you specify hour, minute and second, Typst will store just a time. + /// * If you specify all of year, month, day, hour, minute and second, Typst + /// will store a full datetime. + /// + /// Depending on how it is stored, the [`display`]($datetime.display) method + /// will choose a different formatting by default. + /// + /// ```example + /// #datetime( + /// year: 2012, + /// month: 8, + /// day: 3, + /// ).display() + /// ``` + #[func(constructor)] + pub fn construct( + /// The year of the datetime. + #[named] + year: Option<i32>, + /// The month of the datetime. + #[named] + month: Option<Month>, + /// The day of the datetime. + #[named] + day: Option<u8>, + /// The hour of the datetime. + #[named] + hour: Option<u8>, + /// The minute of the datetime. + #[named] + minute: Option<u8>, + /// The second of the datetime. + #[named] + second: Option<u8>, + ) -> StrResult<Datetime> { + let time = match (hour, minute, second) { + (Some(hour), Some(minute), Some(second)) => { + match time::Time::from_hms(hour, minute, second) { + Ok(time) => Some(time), + Err(_) => bail!("time is invalid"), + } + } + (None, None, None) => None, + _ => bail!("time is incomplete"), + }; + + let date = match (year, month, day) { + (Some(year), Some(month), Some(day)) => { + match time::Date::from_calendar_date(year, month, day) { + Ok(date) => Some(date), + Err(_) => bail!("date is invalid"), + } + } + (None, None, None) => None, + _ => bail!("date is incomplete"), + }; + + Ok(match (date, time) { + (Some(date), Some(time)) => { + Datetime::Datetime(PrimitiveDateTime::new(date, time)) + } + (Some(date), None) => Datetime::Date(date), + (None, Some(time)) => Datetime::Time(time), + (None, None) => { + bail!("at least one of date or time must be fully specified") + } + }) + } + + /// Returns the current date. + /// + /// ```example + /// Today's date is + /// #datetime.today().display(). + /// ``` + #[func] + pub fn today( + /// The engine. + engine: &mut Engine, + /// An offset to apply to the current UTC date. If set to `{auto}`, the + /// offset will be the local offset. + #[named] + #[default] + offset: Smart<i64>, + ) -> StrResult<Datetime> { + Ok(engine + .world + .today(offset.custom()) + .ok_or("unable to get the current date")?) + } + + /// Displays the datetime in a specified format. + /// + /// Depending on whether you have defined just a date, a time or both, the + /// default format will be different. If you specified a date, it will be + /// `[[year]-[month]-[day]]`. If you specified a time, it will be + /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be + /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`. + /// + /// See the [format syntax]($datetime/#format) for more information. + #[func] + pub fn display( + &self, + /// The format used to display the datetime. + #[default] + pattern: Smart<DisplayPattern>, + ) -> StrResult<EcoString> { + let pat = |s| format_description::parse_borrowed::<2>(s).unwrap(); + let result = match pattern { + Smart::Auto => match self { + Self::Date(date) => date.format(&pat("[year]-[month]-[day]")), + Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")), + Self::Datetime(datetime) => { + datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]")) + } + }, + + Smart::Custom(DisplayPattern(_, format)) => match self { + Self::Date(date) => date.format(&format), + Self::Time(time) => time.format(&format), + Self::Datetime(datetime) => datetime.format(&format), + }, + }; + result.map(EcoString::from).map_err(format_time_format_error) + } + + /// The year if it was specified, or `{none}` for times without a date. + #[func] + pub fn year(&self) -> Option<i32> { + match self { + Self::Date(date) => Some(date.year()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.year()), + } + } + + /// The month if it was specified, or `{none}` for times without a date. + #[func] + pub fn month(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.month().into()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.month().into()), + } + } + + /// The weekday (counting Monday as 1) or `{none}` for times without a date. + #[func] + pub fn weekday(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.weekday().number_from_monday()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), + } + } + + /// The day if it was specified, or `{none}` for times without a date. + #[func] + pub fn day(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.day()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.day()), + } + } + + /// The hour if it was specified, or `{none}` for dates without a time. + #[func] + pub fn hour(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.hour()), + Self::Datetime(datetime) => Some(datetime.hour()), + } + } + + /// The minute if it was specified, or `{none}` for dates without a time. + #[func] + pub fn minute(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.minute()), + Self::Datetime(datetime) => Some(datetime.minute()), + } + } + + /// The second if it was specified, or `{none}` for dates without a time. + #[func] + pub fn second(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.second()), + Self::Datetime(datetime) => Some(datetime.second()), + } + } + + /// The ordinal (day of the year), or `{none}` for times without a date. + #[func] + pub fn ordinal(&self) -> Option<u16> { + match self { + Self::Datetime(datetime) => Some(datetime.ordinal()), + Self::Date(date) => Some(date.ordinal()), + Self::Time(_) => None, + } + } +} + +impl Repr for Datetime { + fn repr(&self) -> EcoString { + let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr())); + let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr())); + let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr())); + let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr())); + let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr())); + let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr())); + let filtered = [year, month, day, hour, minute, second] + .into_iter() + .flatten() + .collect::<EcoVec<_>>(); + + eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false)) + } +} + +impl PartialOrd for Datetime { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match (self, other) { + (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b), + (Self::Date(a), Self::Date(b)) => a.partial_cmp(b), + (Self::Time(a), Self::Time(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +impl Add<Duration> for Datetime { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + let rhs: time::Duration = rhs.into(); + match self { + Self::Datetime(datetime) => Self::Datetime(datetime + rhs), + Self::Date(date) => Self::Date(date + rhs), + Self::Time(time) => Self::Time(time + rhs), + } + } +} + +impl Sub<Duration> for Datetime { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + let rhs: time::Duration = rhs.into(); + match self { + Self::Datetime(datetime) => Self::Datetime(datetime - rhs), + Self::Date(date) => Self::Date(date - rhs), + Self::Time(time) => Self::Time(time - rhs), + } + } +} + +impl Sub for Datetime { + type Output = StrResult<Duration>; + + fn sub(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()), + (Self::Date(a), Self::Date(b)) => Ok((a - b).into()), + (Self::Time(a), Self::Time(b)) => Ok((a - b).into()), + (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()), + } + } +} + +/// A format in which a datetime can be displayed. +pub struct DisplayPattern(Str, format_description::OwnedFormatItem); + +cast! { + DisplayPattern, + self => self.0.into_value(), + v: Str => { + let item = format_description::parse_owned::<2>(&v) + .map_err(format_time_invalid_format_description_error)?; + Self(v, item) + } +} + +cast! { + Month, + v: u8 => Self::try_from(v).map_err(|_| "month is invalid")? +} + +/// Format the `Format` error of the time crate in an appropriate way. +fn format_time_format_error(error: Format) -> EcoString { + match error { + Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name), + Format::InsufficientTypeInformation { .. } => { + "failed to format datetime (insufficient information)".into() + } + err => eco_format!("failed to format datetime in the requested format ({err})"), + } +} + +/// Format the `InvalidFormatDescription` error of the time crate in an +/// appropriate way. +fn format_time_invalid_format_description_error( + error: InvalidFormatDescription, +) -> EcoString { + match error { + InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => { + eco_format!("missing closing bracket for bracket at index {}", index) + } + InvalidFormatDescription::InvalidComponentName { name, index, .. } => { + eco_format!("invalid component name '{}' at index {}", name, index) + } + InvalidFormatDescription::InvalidModifier { value, index, .. } => { + eco_format!("invalid modifier '{}' at index {}", value, index) + } + InvalidFormatDescription::Expected { what, index, .. } => { + eco_format!("expected {} at index {}", what, index) + } + InvalidFormatDescription::MissingComponentName { index, .. } => { + eco_format!("expected component name at index {}", index) + } + InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => { + eco_format!( + "missing required modifier {} for component at index {}", + name, + index + ) + } + InvalidFormatDescription::NotSupported { context, what, index, .. } => { + eco_format!("{} is not supported in {} at index {}", what, context, index) + } + err => eco_format!("failed to parse datetime format ({err})"), + } +} diff --git a/crates/typst-library/src/foundations/decimal.rs b/crates/typst-library/src/foundations/decimal.rs new file mode 100644 index 00000000..cf11e1dd --- /dev/null +++ b/crates/typst-library/src/foundations/decimal.rs @@ -0,0 +1,499 @@ +use std::fmt::{self, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Neg; +use std::str::FromStr; + +use ecow::{eco_format, EcoString}; +use rust_decimal::MathematicalOps; +use typst_syntax::{ast, Span, Spanned}; + +use crate::diag::{warning, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{cast, func, repr, scope, ty, Repr, Str}; +use crate::World; + +/// A fixed-point decimal number type. +/// +/// This type should be used for precise arithmetic operations on numbers +/// represented in base 10. A typical use case is representing currency. +/// +/// # Example +/// ```example +/// Decimal: #(decimal("0.1") + decimal("0.2")) \ +/// Float: #(0.1 + 0.2) +/// ``` +/// +/// # Construction and casts +/// To create a decimal number, use the `{decimal(string)}` constructor, such as +/// in `{decimal("3.141592653")}` **(note the double quotes!)**. This +/// constructor preserves all given fractional digits, provided they are +/// representable as per the limits specified below (otherwise, an error is +/// raised). +/// +/// You can also convert any [integer]($int) to a decimal with the +/// `{decimal(int)}` constructor, e.g. `{decimal(59)}`. However, note that +/// constructing a decimal from a [floating-point number]($float), while +/// supported, **is an imprecise conversion and therefore discouraged.** A +/// warning will be raised if Typst detects that there was an accidental `float` +/// to `decimal` cast through its constructor, e.g. if writing `{decimal(3.14)}` +/// (note the lack of double quotes, indicating this is an accidental `float` +/// cast and therefore imprecise). It is recommended to use strings for +/// constant decimal values instead (e.g. `{decimal("3.14")}`). +/// +/// The precision of a `float` to `decimal` cast can be slightly improved by +/// rounding the result to 15 digits with [`calc.round`]($calc.round), but there +/// are still no precision guarantees for that kind of conversion. +/// +/// # Operations +/// Basic arithmetic operations are supported on two decimals and on pairs of +/// decimals and integers. +/// +/// Built-in operations between `float` and `decimal` are not supported in order +/// to guard against accidental loss of precision. They will raise an error +/// instead. +/// +/// Certain `calc` functions, such as trigonometric functions and power between +/// two real numbers, are also only supported for `float` (although raising +/// `decimal` to integer exponents is supported). You can opt into potentially +/// imprecise operations with the `{float(decimal)}` constructor, which casts +/// the `decimal` number into a `float`, allowing for operations without +/// precision guarantees. +/// +/// # Displaying decimals +/// To display a decimal, simply insert the value into the document. To only +/// display a certain number of digits, [round]($calc.round) the decimal first. +/// Localized formatting of decimals and other numbers is not yet supported, but +/// planned for the future. +/// +/// You can convert decimals to strings using the [`str`] constructor. This way, +/// you can post-process the displayed representation, e.g. to replace the +/// period with a comma (as a stand-in for proper built-in localization to +/// languages that use the comma). +/// +/// # Precision and limits +/// A `decimal` number has a limit of 28 to 29 significant base-10 digits. This +/// includes the sum of digits before and after the decimal point. As such, +/// numbers with more fractional digits have a smaller range. The maximum and +/// minimum `decimal` numbers have a value of `{79228162514264337593543950335}` +/// and `{-79228162514264337593543950335}` respectively. In contrast with +/// [`float`], this type does not support infinity or NaN, so overflowing or +/// underflowing operations will raise an error. +/// +/// Typical operations between `decimal` numbers, such as addition, +/// multiplication, and [power]($calc.pow) to an integer, will be highly precise +/// due to their fixed-point representation. Note, however, that multiplication +/// and division may not preserve all digits in some edge cases: while they are +/// considered precise, digits past the limits specified above are rounded off +/// and lost, so some loss of precision beyond the maximum representable digits +/// is possible. Note that this behavior can be observed not only when dividing, +/// but also when multiplying by numbers between 0 and 1, as both operations can +/// push a number's fractional digits beyond the limits described above, leading +/// to rounding. When those two operations do not surpass the digit limits, they +/// are fully precise. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Decimal(rust_decimal::Decimal); + +impl Decimal { + pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO); + pub const ONE: Self = Self(rust_decimal::Decimal::ONE); + pub const MIN: Self = Self(rust_decimal::Decimal::MIN); + pub const MAX: Self = Self(rust_decimal::Decimal::MAX); + + /// Whether this decimal value is zero. + pub const fn is_zero(self) -> bool { + self.0.is_zero() + } + + /// Whether this decimal value is negative. + pub const fn is_negative(self) -> bool { + self.0.is_sign_negative() + } + + /// Whether this decimal has fractional part equal to zero (is an integer). + pub fn is_integer(self) -> bool { + self.0.is_integer() + } + + /// Computes the absolute value of this decimal. + pub fn abs(self) -> Self { + Self(self.0.abs()) + } + + /// Computes the largest integer less than or equal to this decimal. + /// + /// A decimal is returned as this may not be within `i64`'s range of + /// values. + pub fn floor(self) -> Self { + Self(self.0.floor()) + } + + /// Computes the smallest integer greater than or equal to this decimal. + /// + /// A decimal is returned as this may not be within `i64`'s range of + /// values. + pub fn ceil(self) -> Self { + Self(self.0.ceil()) + } + + /// Returns the integer part of this decimal. + pub fn trunc(self) -> Self { + Self(self.0.trunc()) + } + + /// Returns the fractional part of this decimal (with the integer part set + /// to zero). + pub fn fract(self) -> Self { + Self(self.0.fract()) + } + + /// Rounds this decimal up to the specified amount of digits with the + /// traditional rounding rules, using the "midpoint away from zero" + /// strategy (6.5 -> 7, -6.5 -> -7). + /// + /// If given a negative amount of digits, rounds to integer digits instead + /// with the same rounding strategy. For example, rounding to -3 digits + /// will turn 34567.89 into 35000.00 and -34567.89 into -35000.00. + /// + /// Note that this can return `None` when using negative digits where the + /// rounded number would overflow the available range for decimals. + pub fn round(self, digits: i32) -> Option<Self> { + // Positive digits can be handled by just rounding with rust_decimal. + if let Ok(positive_digits) = u32::try_from(digits) { + return Some(Self(self.0.round_dp_with_strategy( + positive_digits, + rust_decimal::RoundingStrategy::MidpointAwayFromZero, + ))); + } + + // We received negative digits, so we round to integer digits. + let mut num = self.0; + let old_scale = num.scale(); + let digits = -digits as u32; + + let (Ok(_), Some(ten_to_digits)) = ( + // Same as dividing by 10^digits. + num.set_scale(old_scale + digits), + rust_decimal::Decimal::TEN.checked_powi(digits as i64), + ) else { + // Scaling more than any possible amount of integer digits. + let mut zero = rust_decimal::Decimal::ZERO; + zero.set_sign_negative(self.is_negative()); + return Some(Self(zero)); + }; + + // Round to this integer digit. + num = num.round_dp_with_strategy( + 0, + rust_decimal::RoundingStrategy::MidpointAwayFromZero, + ); + + // Multiply by 10^digits again, which can overflow and fail. + num.checked_mul(ten_to_digits).map(Self) + } + + /// Attempts to add two decimals. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_add(self, other: Self) -> Option<Self> { + self.0.checked_add(other.0).map(Self) + } + + /// Attempts to subtract a decimal from another. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_sub(self, other: Self) -> Option<Self> { + self.0.checked_sub(other.0).map(Self) + } + + /// Attempts to multiply two decimals. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_mul(self, other: Self) -> Option<Self> { + self.0.checked_mul(other.0).map(Self) + } + + /// Attempts to divide two decimals. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_div(self, other: Self) -> Option<Self> { + self.0.checked_div(other.0).map(Self) + } + + /// Attempts to obtain the quotient of Euclidean division between two + /// decimals. Implemented similarly to [`f64::div_euclid`]. + /// + /// The returned quotient is truncated and adjusted if the remainder was + /// negative. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_div_euclid(self, other: Self) -> Option<Self> { + let q = self.0.checked_div(other.0)?.trunc(); + if self + .0 + .checked_rem(other.0) + .as_ref() + .is_some_and(rust_decimal::Decimal::is_sign_negative) + { + return if other.0.is_sign_positive() { + q.checked_sub(rust_decimal::Decimal::ONE).map(Self) + } else { + q.checked_add(rust_decimal::Decimal::ONE).map(Self) + }; + } + Some(Self(q)) + } + + /// Attempts to obtain the remainder of Euclidean division between two + /// decimals. Implemented similarly to [`f64::rem_euclid`]. + /// + /// The returned decimal `r` is non-negative within the range + /// `0.0 <= r < other.abs()`. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_rem_euclid(self, other: Self) -> Option<Self> { + let r = self.0.checked_rem(other.0)?; + Some(Self(if r.is_sign_negative() { r.checked_add(other.0.abs())? } else { r })) + } + + /// Attempts to calculate the remainder of the division of two decimals. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_rem(self, other: Self) -> Option<Self> { + self.0.checked_rem(other.0).map(Self) + } + + /// Attempts to take one decimal to the power of an integer. + /// + /// Returns `None` for invalid operands, as well as on overflow or + /// underflow. + pub fn checked_powi(self, other: i64) -> Option<Self> { + self.0.checked_powi(other).map(Self) + } +} + +#[scope] +impl Decimal { + /// Converts a value to a `decimal`. + /// + /// It is recommended to use a string to construct the decimal number, or an + /// [integer]($int) (if desired). The string must contain a number in the + /// format `{"3.14159"}` (or `{"-3.141519"}` for negative numbers). The + /// fractional digits are fully preserved; if that's not possible due to the + /// limit of significant digits (around 28 to 29) having been reached, an + /// error is raised as the given decimal number wouldn't be representable. + /// + /// While this constructor can be used with [floating-point numbers]($float) + /// to cast them to `decimal`, doing so is **discouraged** as **this cast is + /// inherently imprecise.** It is easy to accidentally perform this cast by + /// writing `{decimal(1.234)}` (note the lack of double quotes), which is + /// why Typst will emit a warning in that case. Please write + /// `{decimal("1.234")}` instead for that particular case (initialization of + /// a constant decimal). Also note that floats that are NaN or infinite + /// cannot be cast to decimals and will raise an error. + /// + /// ```example + /// #decimal("1.222222222222222") + /// ``` + #[func(constructor)] + pub fn construct( + engine: &mut Engine, + /// The value that should be converted to a decimal. + value: Spanned<ToDecimal>, + ) -> SourceResult<Decimal> { + match value.v { + ToDecimal::Str(str) => Self::from_str(&str.replace(repr::MINUS_SIGN, "-")) + .map_err(|_| eco_format!("invalid decimal: {str}")) + .at(value.span), + ToDecimal::Int(int) => Ok(Self::from(int)), + ToDecimal::Float(float) => { + warn_on_float_literal(engine, value.span); + Self::try_from(float) + .map_err(|_| { + eco_format!( + "float is not a valid decimal: {}", + repr::format_float(float, None, true, "") + ) + }) + .at(value.span) + } + } + } +} + +/// Emits a warning when a decimal is constructed from a float literal. +fn warn_on_float_literal(engine: &mut Engine, span: Span) -> Option<()> { + let id = span.id()?; + let source = engine.world.source(id).ok()?; + let node = source.find(span)?; + if node.is::<ast::Float>() { + engine.sink.warn(warning!( + span, + "creating a decimal using imprecise float literal"; + hint: "use a string in the decimal constructor to avoid loss \ + of precision: `decimal({})`", + node.text().repr() + )); + } + Some(()) +} + +impl FromStr for Decimal { + type Err = rust_decimal::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + rust_decimal::Decimal::from_str_exact(s).map(Self) + } +} + +impl From<i64> for Decimal { + fn from(value: i64) -> Self { + Self(rust_decimal::Decimal::from(value)) + } +} + +impl TryFrom<f64> for Decimal { + type Error = (); + + /// Attempts to convert a Decimal to a float. + /// + /// This can fail if the float is infinite or NaN, or otherwise cannot be + /// represented by a decimal number. + fn try_from(value: f64) -> Result<Self, Self::Error> { + rust_decimal::Decimal::from_f64_retain(value).map(Self).ok_or(()) + } +} + +impl TryFrom<Decimal> for f64 { + type Error = rust_decimal::Error; + + /// Attempts to convert a Decimal to a float. + /// + /// This should in principle be infallible according to the implementation, + /// but we mirror the decimal implementation's API either way. + fn try_from(value: Decimal) -> Result<Self, Self::Error> { + value.0.try_into() + } +} + +impl TryFrom<Decimal> for i64 { + type Error = rust_decimal::Error; + + /// Attempts to convert a Decimal to an integer. + /// + /// Returns an error if the decimal has a fractional part, or if there + /// would be overflow or underflow. + fn try_from(value: Decimal) -> Result<Self, Self::Error> { + value.0.try_into() + } +} + +impl Display for Decimal { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.0.is_sign_negative() { + f.write_str(repr::MINUS_SIGN)?; + } + self.0.abs().fmt(f) + } +} + +impl Repr for Decimal { + fn repr(&self) -> EcoString { + eco_format!("decimal({})", eco_format!("{}", self.0).repr()) + } +} + +impl Neg for Decimal { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Hash for Decimal { + fn hash<H: Hasher>(&self, state: &mut H) { + // `rust_decimal`'s Hash implementation normalizes decimals before + // hashing them. This means decimals with different scales but + // equivalent value not only compare equal but also hash equally. Here, + // we hash all bytes explicitly to ensure the scale is also considered. + // This means that 123.314 == 123.31400, but 123.314.hash() != + // 123.31400.hash(). + // + // Note that this implies that equal decimals can have different hashes, + // which might generate problems with certain data structures, such as + // HashSet and HashMap. + self.0.serialize().hash(state); + } +} + +/// A value that can be cast to a decimal. +pub enum ToDecimal { + /// A string with the decimal's representation. + Str(EcoString), + /// An integer to be converted to the equivalent decimal. + Int(i64), + /// A float to be converted to the equivalent decimal. + Float(f64), +} + +cast! { + ToDecimal, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Str => Self::Str(EcoString::from(v)), +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use typst_utils::hash128; + + use super::Decimal; + + #[test] + fn test_decimals_with_equal_scales_hash_identically() { + let a = Decimal::from_str("3.14").unwrap(); + let b = Decimal::from_str("3.14").unwrap(); + assert_eq!(a, b); + assert_eq!(hash128(&a), hash128(&b)); + } + + #[test] + fn test_decimals_with_different_scales_hash_differently() { + let a = Decimal::from_str("3.140").unwrap(); + let b = Decimal::from_str("3.14000").unwrap(); + assert_eq!(a, b); + assert_ne!(hash128(&a), hash128(&b)); + } + + #[track_caller] + fn test_round(value: &str, digits: i32, expected: &str) { + assert_eq!( + Decimal::from_str(value).unwrap().round(digits), + Some(Decimal::from_str(expected).unwrap()), + ); + } + + #[test] + fn test_decimal_positive_round() { + test_round("312.55553", 0, "313.00000"); + test_round("312.55553", 3, "312.556"); + test_round("312.5555300000", 3, "312.556"); + test_round("-312.55553", 3, "-312.556"); + test_round("312.55553", 28, "312.55553"); + test_round("312.55553", 2341, "312.55553"); + test_round("-312.55553", 2341, "-312.55553"); + } + + #[test] + fn test_decimal_negative_round() { + test_round("4596.55553", -1, "4600"); + test_round("4596.555530000000", -1, "4600"); + test_round("-4596.55553", -3, "-5000"); + test_round("4596.55553", -28, "0"); + test_round("-4596.55553", -2341, "0"); + assert_eq!(Decimal::MAX.round(-1), None); + } +} diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs new file mode 100644 index 00000000..e4ab54e7 --- /dev/null +++ b/crates/typst-library/src/foundations/dict.rs @@ -0,0 +1,395 @@ +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::is_ident; +use typst_utils::ArcExt; + +use crate::diag::{Hint, HintedStrResult, StrResult}; +use crate::foundations::{ + array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value, +}; + +/// Create a new [`Dict`] from key-value pairs. +#[macro_export] +#[doc(hidden)] +macro_rules! __dict { + ($($key:expr => $value:expr),* $(,)?) => {{ + #[allow(unused_mut)] + let mut map = $crate::foundations::IndexMap::new(); + $(map.insert($key.into(), $crate::foundations::IntoValue::into_value($value));)* + $crate::foundations::Dict::from(map) + }}; +} + +#[doc(inline)] +pub use crate::__dict as dict; + +/// A map from string keys to values. +/// +/// You can construct a dictionary by enclosing comma-separated `key: value` +/// pairs in parentheses. The values do not have to be of the same type. Since +/// empty parentheses already yield an empty array, you have to use the special +/// `(:)` syntax to create an empty dictionary. +/// +/// A dictionary is conceptually similar to an array, but it is indexed by +/// strings instead of integers. You can access and create dictionary entries +/// with the `.at()` method. If you know the key statically, you can +/// alternatively use [field access notation]($scripting/#fields) (`.key`) to +/// access the value. Dictionaries can be added with the `+` operator and +/// [joined together]($scripting/#blocks). To check whether a key is present in +/// the dictionary, use the `in` keyword. +/// +/// You can iterate over the pairs in a dictionary using a [for +/// loop]($scripting/#loops). This will iterate in the order the pairs were +/// inserted / declared. +/// +/// # Example +/// ```example +/// #let dict = ( +/// name: "Typst", +/// born: 2019, +/// ) +/// +/// #dict.name \ +/// #(dict.launch = 20) +/// #dict.len() \ +/// #dict.keys() \ +/// #dict.values() \ +/// #dict.at("born") \ +/// #dict.insert("city", "Berlin ") +/// #("name" in dict) +/// ``` +#[ty(scope, cast, name = "dictionary")] +#[derive(Default, Clone, PartialEq)] +pub struct Dict(Arc<IndexMap<Str, Value>>); + +impl Dict { + /// Create a new, empty dictionary. + pub fn new() -> Self { + Self::default() + } + + /// Whether the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Borrow the value at the given key. + pub fn get(&self, key: &str) -> StrResult<&Value> { + self.0.get(key).ok_or_else(|| missing_key(key)) + } + + /// Mutably borrow the value the given `key` maps to. + pub fn at_mut(&mut self, key: &str) -> HintedStrResult<&mut Value> { + Arc::make_mut(&mut self.0) + .get_mut(key) + .ok_or_else(|| missing_key(key)) + .hint("use `insert` to add or update values") + } + + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .shift_remove(key) + .ok_or_else(|| missing_key(key)) + } + + /// Whether the dictionary contains a specific key. + pub fn contains(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Clear the dictionary. + pub fn clear(&mut self) { + if Arc::strong_count(&self.0) == 1 { + Arc::make_mut(&mut self.0).clear(); + } else { + *self = Self::new(); + } + } + + /// Iterate over pairs of references to the contained keys and values. + pub fn iter(&self) -> indexmap::map::Iter<Str, Value> { + self.0.iter() + } + + /// Check if there is any remaining pair, and if so return an + /// "unexpected key" error. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + let mut iter = self.iter().peekable(); + if iter.peek().is_none() { + return Ok(()); + } + let unexpected: Vec<&str> = iter.map(|kv| kv.0.as_str()).collect(); + + Err(Self::unexpected_keys(unexpected, Some(expected))) + } + + // Return an "unexpected key" error string. + pub fn unexpected_keys( + unexpected: Vec<&str>, + hint_expected: Option<&[&str]>, + ) -> EcoString { + let format_as_list = |arr: &[&str]| { + repr::separated_list( + &arr.iter().map(|s| eco_format!("\"{s}\"")).collect::<Vec<_>>(), + "and", + ) + }; + + let mut msg = String::from(match unexpected.len() { + 1 => "unexpected key ", + _ => "unexpected keys ", + }); + + msg.push_str(&format_as_list(&unexpected[..])); + + if let Some(expected) = hint_expected { + msg.push_str(", valid keys are "); + msg.push_str(&format_as_list(expected)); + } + + msg.into() + } +} + +#[scope] +impl Dict { + /// Converts a value into a dictionary. + /// + /// Note that this function is only intended for conversion of a + /// dictionary-like value to a dictionary, not for creation of a dictionary + /// from individual pairs. Use the dictionary syntax `(key: value)` instead. + /// + /// ```example + /// #dictionary(sys).at("version") + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a dictionary. + value: ToDict, + ) -> Dict { + value.0 + } + + /// The number of pairs in the dictionary. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the value associated with the specified key in the dictionary. + /// May be used on the left-hand side of an assignment if the key is already + /// present in the dictionary. Returns the default value if the key is not + /// part of the dictionary or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The key at which to retrieve the item. + key: Str, + /// A default value to return if the key is not part of the dictionary. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.0 + .get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(&key)) + } + + /// Inserts a new pair into the dictionary. If the dictionary already + /// contains this key, the value is updated. + #[func] + pub fn insert( + &mut self, + /// The key of the pair that should be inserted. + key: Str, + /// The value of the pair that should be inserted. + value: Value, + ) { + Arc::make_mut(&mut self.0).insert(key, value); + } + + /// Removes a pair from the dictionary by key and return the value. + #[func] + pub fn remove( + &mut self, + /// The key of the pair to remove. + key: Str, + /// A default value to return if the key does not exist. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .shift_remove(&key) + .or(default) + .ok_or_else(|| missing_key(&key)) + } + + /// Returns the keys of the dictionary as an array in insertion order. + #[func] + pub fn keys(&self) -> Array { + self.0.keys().cloned().map(Value::Str).collect() + } + + /// Returns the values of the dictionary as an array in insertion order. + #[func] + pub fn values(&self) -> Array { + self.0.values().cloned().collect() + } + + /// Returns the keys and values of the dictionary as an array of pairs. Each + /// pair is represented as an array of length two. + #[func] + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) + .collect() + } +} + +/// A value that can be cast to dictionary. +pub struct ToDict(Dict); + +cast! { + ToDict, + v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), +} + +impl Debug for Dict { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.0.iter()).finish() + } +} + +impl Repr for Dict { + fn repr(&self) -> EcoString { + if self.is_empty() { + return "(:)".into(); + } + + let max = 40; + let mut pieces: Vec<_> = self + .iter() + .take(max) + .map(|(key, value)| { + if is_ident(key) { + eco_format!("{key}: {}", value.repr()) + } else { + eco_format!("{}: {}", key.repr(), value.repr()) + } + }) + .collect(); + + if self.len() > max { + pieces.push(eco_format!(".. ({} pairs omitted)", self.len() - max)); + } + + repr::pretty_array_like(&pieces, false).into() + } +} + +impl Add for Dict { + type Output = Self; + + fn add(mut self, rhs: Dict) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Dict { + fn add_assign(&mut self, rhs: Dict) { + match Arc::try_unwrap(rhs.0) { + Ok(map) => self.extend(map), + Err(rc) => self.extend(rc.iter().map(|(k, v)| (k.clone(), v.clone()))), + } + } +} + +impl Hash for Dict { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.0.len()); + for item in self { + item.hash(state); + } + } +} + +impl Serialize for Dict { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Dict { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Ok(IndexMap::<Str, Value>::deserialize(deserializer)?.into()) + } +} + +impl Extend<(Str, Value)> for Dict { + fn extend<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) { + Arc::make_mut(&mut self.0).extend(iter); + } +} + +impl FromIterator<(Str, Value)> for Dict { + fn from_iter<T: IntoIterator<Item = (Str, Value)>>(iter: T) -> Self { + Self(Arc::new(iter.into_iter().collect())) + } +} + +impl IntoIterator for Dict { + type Item = (Str, Value); + type IntoIter = indexmap::map::IntoIter<Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + Arc::take(self.0).into_iter() + } +} + +impl<'a> IntoIterator for &'a Dict { + type Item = (&'a Str, &'a Value); + type IntoIter = indexmap::map::Iter<'a, Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl From<IndexMap<Str, Value>> for Dict { + fn from(map: IndexMap<Str, Value>) -> Self { + Self(Arc::new(map)) + } +} + +/// The missing key access error message. +#[cold] +fn missing_key(key: &str) -> EcoString { + eco_format!("dictionary does not contain key {}", key.repr()) +} + +/// The missing key access error message when no default was given. +#[cold] +fn missing_key_no_default(key: &str) -> EcoString { + eco_format!( + "dictionary does not contain key {} \ + and no default value was specified", + key.repr() + ) +} diff --git a/crates/typst-library/src/foundations/duration.rs b/crates/typst-library/src/foundations/duration.rs new file mode 100644 index 00000000..94d44fb2 --- /dev/null +++ b/crates/typst-library/src/foundations/duration.rs @@ -0,0 +1,215 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +use ecow::{eco_format, EcoString}; +use time::ext::NumericalDuration; + +use crate::foundations::{func, repr, scope, ty, Repr}; + +/// Represents a positive or negative span of time. +#[ty(scope, cast)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Duration(time::Duration); + +impl Duration { + /// Whether the duration is empty / zero. + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +#[scope] +impl Duration { + /// Creates a new duration. + /// + /// You can specify the [duration] using weeks, days, hours, minutes and + /// seconds. You can also get a duration by subtracting two + /// [datetimes]($datetime). + /// + /// ```example + /// #duration( + /// days: 3, + /// hours: 12, + /// ).hours() + /// ``` + #[func(constructor)] + pub fn construct( + /// The number of seconds. + #[named] + #[default(0)] + seconds: i64, + /// The number of minutes. + #[named] + #[default(0)] + minutes: i64, + /// The number of hours. + #[named] + #[default(0)] + hours: i64, + /// The number of days. + #[named] + #[default(0)] + days: i64, + /// The number of weeks. + #[named] + #[default(0)] + weeks: i64, + ) -> Duration { + Duration::from( + time::Duration::seconds(seconds) + + time::Duration::minutes(minutes) + + time::Duration::hours(hours) + + time::Duration::days(days) + + time::Duration::weeks(weeks), + ) + } + + /// The duration expressed in seconds. + /// + /// This function returns the total duration represented in seconds as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn seconds(&self) -> f64 { + self.0.as_seconds_f64() + } + + /// The duration expressed in minutes. + /// + /// This function returns the total duration represented in minutes as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn minutes(&self) -> f64 { + self.seconds() / 60.0 + } + + /// The duration expressed in hours. + /// + /// This function returns the total duration represented in hours as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn hours(&self) -> f64 { + self.seconds() / 3_600.0 + } + + /// The duration expressed in days. + /// + /// This function returns the total duration represented in days as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn days(&self) -> f64 { + self.seconds() / 86_400.0 + } + + /// The duration expressed in weeks. + /// + /// This function returns the total duration represented in weeks as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn weeks(&self) -> f64 { + self.seconds() / 604_800.0 + } +} + +impl Debug for Duration { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Repr for Duration { + fn repr(&self) -> EcoString { + let mut tmp = self.0; + let mut vec = Vec::with_capacity(5); + + let weeks = tmp.whole_seconds() / 604_800.0 as i64; + if weeks != 0 { + vec.push(eco_format!("weeks: {}", weeks.repr())); + } + tmp -= weeks.weeks(); + + let days = tmp.whole_days(); + if days != 0 { + vec.push(eco_format!("days: {}", days.repr())); + } + tmp -= days.days(); + + let hours = tmp.whole_hours(); + if hours != 0 { + vec.push(eco_format!("hours: {}", hours.repr())); + } + tmp -= hours.hours(); + + let minutes = tmp.whole_minutes(); + if minutes != 0 { + vec.push(eco_format!("minutes: {}", minutes.repr())); + } + tmp -= minutes.minutes(); + + let seconds = tmp.whole_seconds(); + if seconds != 0 { + vec.push(eco_format!("seconds: {}", seconds.repr())); + } + + eco_format!("duration{}", &repr::pretty_array_like(&vec, false)) + } +} + +impl From<time::Duration> for Duration { + fn from(value: time::Duration) -> Self { + Self(value) + } +} + +impl From<Duration> for time::Duration { + fn from(value: Duration) -> Self { + value.0 + } +} + +impl Add for Duration { + type Output = Duration; + + fn add(self, rhs: Self) -> Self::Output { + Duration(self.0 + rhs.0) + } +} + +impl Sub for Duration { + type Output = Duration; + + fn sub(self, rhs: Self) -> Self::Output { + Duration(self.0 - rhs.0) + } +} + +impl Neg for Duration { + type Output = Duration; + + fn neg(self) -> Self::Output { + Duration(-self.0) + } +} + +impl Mul<f64> for Duration { + type Output = Duration; + + fn mul(self, rhs: f64) -> Self::Output { + Duration(self.0 * rhs) + } +} + +impl Div<f64> for Duration { + type Output = Duration; + + fn div(self, rhs: f64) -> Self::Output { + Duration(self.0 / rhs) + } +} + +impl Div for Duration { + type Output = f64; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} diff --git a/crates/typst-library/src/foundations/element.rs b/crates/typst-library/src/foundations/element.rs new file mode 100644 index 00000000..8da71965 --- /dev/null +++ b/crates/typst-library/src/foundations/element.rs @@ -0,0 +1,333 @@ +use std::any::TypeId; +use std::cmp::Ordering; +use std::fmt::{self, Debug}; +use std::hash::Hash; +use std::ptr::NonNull; + +use ecow::EcoString; +use once_cell::sync::Lazy; +use smallvec::SmallVec; +#[doc(inline)] +pub use typst_macros::elem; +use typst_utils::Static; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, Args, Content, Dict, FieldAccessError, Func, ParamInfo, Repr, Scope, Selector, + StyleChain, Styles, Value, +}; +use crate::text::{Lang, Region}; + +/// A document element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Element(Static<NativeElementData>); + +impl Element { + /// Get the element for `T`. + pub fn of<T: NativeElement>() -> Self { + T::elem() + } + + /// The element's normal name (e.g. `enum`). + pub fn name(self) -> &'static str { + self.0.name + } + + /// The element's title case name, for use in documentation + /// (e.g. `Numbered List`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the element (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs + } + + /// Search keywords for the element. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// Construct an instance of this element. + pub fn construct( + self, + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<Content> { + (self.0.construct)(engine, args) + } + + /// Execute the set rule for the element and return the resulting style map. + pub fn set(self, engine: &mut Engine, mut args: Args) -> SourceResult<Styles> { + let styles = (self.0.set)(engine, &mut args)?; + args.finish()?; + Ok(styles) + } + + /// Whether the element has the given capability. + pub fn can<C>(self) -> bool + where + C: ?Sized + 'static, + { + self.can_type_id(TypeId::of::<C>()) + } + + /// Whether the element has the given capability where the capability is + /// given by a `TypeId`. + pub fn can_type_id(self, type_id: TypeId) -> bool { + (self.0.vtable)(type_id).is_some() + } + + /// The VTable for capabilities dispatch. + pub fn vtable(self) -> fn(of: TypeId) -> Option<NonNull<()>> { + self.0.vtable + } + + /// Create a selector for this element. + pub fn select(self) -> Selector { + Selector::Elem(self, None) + } + + /// Create a selector for this element, filtering for those that + /// [fields](crate::foundations::Content::field) match the given argument. + pub fn where_(self, fields: SmallVec<[(u8, Value); 1]>) -> Selector { + Selector::Elem(self, Some(fields)) + } + + /// The element's associated scope of sub-definition. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Details about the element's fields. + pub fn params(&self) -> &'static [ParamInfo] { + &(self.0).0.params + } + + /// Extract the field ID for the given field name. + pub fn field_id(&self, name: &str) -> Option<u8> { + if name == "label" { + return Some(255); + } + (self.0.field_id)(name) + } + + /// Extract the field name for the given field ID. + pub fn field_name(&self, id: u8) -> Option<&'static str> { + if id == 255 { + return Some("label"); + } + (self.0.field_name)(id) + } + + /// Extract the value of the field for the given field ID and style chain. + pub fn field_from_styles( + &self, + id: u8, + styles: StyleChain, + ) -> Result<Value, FieldAccessError> { + (self.0.field_from_styles)(id, styles) + } + + /// The element's local name, if any. + pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> { + (self.0).0.local_name.map(|f| f(lang, region)) + } +} + +impl Debug for Element { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Element({})", self.name()) + } +} + +impl Repr for Element { + fn repr(&self) -> EcoString { + self.name().into() + } +} + +impl Ord for Element { + fn cmp(&self, other: &Self) -> Ordering { + self.name().cmp(other.name()) + } +} + +impl PartialOrd for Element { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +cast! { + Element, + self => Value::Func(self.into()), + v: Func => v.element().ok_or("expected element")?, +} + +/// A Typst element that is defined by a native Rust type. +pub trait NativeElement: + Debug + + Clone + + PartialEq + + Hash + + Construct + + Set + + Capable + + Fields + + Repr + + Send + + Sync + + 'static +{ + /// Get the element for the native Rust element. + fn elem() -> Element + where + Self: Sized, + { + Element::from(Self::data()) + } + + /// Pack the element into type-erased content. + fn pack(self) -> Content + where + Self: Sized, + { + Content::new(self) + } + + /// Get the element data for the native Rust element. + fn data() -> &'static NativeElementData + where + Self: Sized; +} + +/// Used to cast an element to a trait object for a trait it implements. +/// +/// # Safety +/// If the `vtable` function returns `Some(p)`, then `p` must be a valid pointer +/// to a vtable of `Packed<Self>` w.r.t to the trait `C` where `capability` is +/// `TypeId::of::<dyn C>()`. +pub unsafe trait Capable { + /// Get the pointer to the vtable for the given capability / trait. + fn vtable(capability: TypeId) -> Option<NonNull<()>>; +} + +/// Defines how fields of an element are accessed. +pub trait Fields { + /// An enum with the fields of the element. + type Enum + where + Self: Sized; + + /// Whether the element has the given field set. + fn has(&self, id: u8) -> bool; + + /// Get the field with the given field ID. + fn field(&self, id: u8) -> Result<Value, FieldAccessError>; + + /// Get the field with the given ID in the presence of styles. + fn field_with_styles( + &self, + id: u8, + styles: StyleChain, + ) -> Result<Value, FieldAccessError>; + + /// Get the field with the given ID from the styles. + fn field_from_styles(id: u8, styles: StyleChain) -> Result<Value, FieldAccessError> + where + Self: Sized; + + /// Resolve all fields with the styles and save them in-place. + fn materialize(&mut self, styles: StyleChain); + + /// Get the fields of the element. + fn fields(&self) -> Dict; +} + +/// An element's constructor function. +pub trait Construct { + /// Construct an element from the arguments. + /// + /// This is passed only the arguments that remain after execution of the + /// element's set rule. + fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> + where + Self: Sized; +} + +/// An element's set rule. +pub trait Set { + /// Parse relevant arguments into style properties for this element. + fn set(engine: &mut Engine, args: &mut Args) -> SourceResult<Styles> + where + Self: Sized; +} + +/// Defines a native element. +#[derive(Debug)] +pub struct NativeElementData { + /// The element's normal name (e.g. `align`), as exposed to Typst. + pub name: &'static str, + /// The element's title case name (e.g. `Align`). + pub title: &'static str, + /// The documentation for this element as a string. + pub docs: &'static str, + /// A list of alternate search terms for this element. + pub keywords: &'static [&'static str], + /// The constructor for this element (see [`Construct`]). + pub construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>, + /// Executes this element's set rule (see [`Set`]). + pub set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>, + /// Gets the vtable for one of this element's capabilities + /// (see [`Capable`]). + pub vtable: fn(capability: TypeId) -> Option<NonNull<()>>, + /// Gets the numeric index of this field by its name. + pub field_id: fn(name: &str) -> Option<u8>, + /// Gets the name of a field by its numeric index. + pub field_name: fn(u8) -> Option<&'static str>, + /// Get the field with the given ID in the presence of styles (see [`Fields`]). + pub field_from_styles: fn(u8, StyleChain) -> Result<Value, FieldAccessError>, + /// Gets the localized name for this element (see [`LocalName`][crate::text::LocalName]). + pub local_name: Option<fn(Lang, Option<Region>) -> &'static str>, + pub scope: Lazy<Scope>, + /// A list of parameter information for each field. + pub params: Lazy<Vec<ParamInfo>>, +} + +impl From<&'static NativeElementData> for Element { + fn from(data: &'static NativeElementData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeElementData, + self => Element::from(self).into_value(), +} + +/// Synthesize fields on an element. This happens before execution of any show +/// rule. +pub trait Synthesize { + /// Prepare the element for show rule application. + fn synthesize(&mut self, engine: &mut Engine, styles: StyleChain) + -> SourceResult<()>; +} + +/// Defines a built-in show rule for an element. +pub trait Show { + /// Execute the base recipe for this element. + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content>; +} + +/// Defines built-in show set rules for an element. +/// +/// This is a bit more powerful than a user-defined show-set because it can +/// access the element's fields. +pub trait ShowSet { + /// Finalize the fully realized form of the element. Use this for effects + /// that should work even in the face of a user-defined show rule. + fn show_set(&self, styles: StyleChain) -> Styles; +} diff --git a/crates/typst-library/src/foundations/fields.rs b/crates/typst-library/src/foundations/fields.rs new file mode 100644 index 00000000..422f30b8 --- /dev/null +++ b/crates/typst-library/src/foundations/fields.rs @@ -0,0 +1,91 @@ +//! Fields on values. + +use ecow::{eco_format, EcoString}; + +use crate::diag::StrResult; +use crate::foundations::{IntoValue, Type, Value, Version}; +use crate::layout::{Alignment, Length, Rel}; +use crate::visualize::Stroke; + +/// 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 ty = value.ty(); + let nope = || Err(no_fields(ty)); + let missing = || Err(missing_field(ty, field)); + + // Special cases, such as module and dict, are handled by Value itself + let result = match value { + Value::Version(version) => match version.component(field) { + Ok(i) => i.into_value(), + Err(_) => return missing(), + }, + Value::Length(length) => match field { + "em" => length.em.get().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::<Stroke>() { + match field { + "paint" => stroke.paint.clone().into_value(), + "thickness" => stroke.thickness.into_value(), + "cap" => stroke.cap.into_value(), + "join" => stroke.join.into_value(), + "dash" => stroke.dash.clone().into_value(), + "miter-limit" => { + stroke.miter_limit.map(|limit| limit.get()).into_value() + } + _ => return missing(), + } + } else if let Some(align) = dynamic.downcast::<Alignment>() { + match field { + "x" => align.x().into_value(), + "y" => align.y().into_value(), + _ => return missing(), + } + } else { + return nope(); + } + } + _ => return nope(), + }; + + Ok(result) +} + +/// The error message for a type not supporting field access. +#[cold] +fn no_fields(ty: Type) -> EcoString { + eco_format!("cannot access fields on type {ty}") +} + +/// The missing field error message. +#[cold] +fn missing_field(ty: Type, field: &str) -> EcoString { + eco_format!("{ty} does not contain field \"{field}\"") +} + +/// List the available fields for a type. +pub fn fields_on(ty: Type) -> &'static [&'static str] { + if ty == Type::of::<Version>() { + &Version::COMPONENTS + } else if ty == Type::of::<Length>() { + &["em", "abs"] + } else if ty == Type::of::<Rel>() { + &["ratio", "length"] + } else if ty == Type::of::<Stroke>() { + &["paint", "thickness", "cap", "join", "dash", "miter-limit"] + } else if ty == Type::of::<Alignment>() { + &["x", "y"] + } else { + &[] + } +} diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs new file mode 100644 index 00000000..bb3232ee --- /dev/null +++ b/crates/typst-library/src/foundations/float.rs @@ -0,0 +1,190 @@ +use std::num::ParseFloatError; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str, +}; +use crate::layout::Ratio; + +/// A floating-point number. +/// +/// A limited-precision representation of a real number. Typst uses 64 bits to +/// store floats. Wherever a float is expected, you can also pass an +/// [integer]($int). +/// +/// You can convert a value to a float with this type's constructor. +/// +/// NaN and positive infinity are available as `{float.nan}` and `{float.inf}` +/// respectively. +/// +/// # Example +/// ```example +/// #3.14 \ +/// #1e4 \ +/// #(10 / 4) +/// ``` +#[ty(scope, cast, name = "float")] +type f64; + +#[scope] +impl f64 { + /// Positive infinity. + const INF: f64 = f64::INFINITY; + + /// A NaN value, as defined by the + /// [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754). + const NAN: f64 = f64::NAN; + + /// Converts a value to a float. + /// + /// - Booleans are converted to `0.0` or `1.0`. + /// - Integers are converted to the closest 64-bit float. For integers with + /// absolute value less than `{calc.pow(2, 53)}`, this conversion is + /// exact. + /// - Ratios are divided by 100%. + /// - Strings are parsed in base 10 to the closest 64-bit float. Exponential + /// notation is supported. + /// + /// ```example + /// #float(false) \ + /// #float(true) \ + /// #float(4) \ + /// #float(40%) \ + /// #float("2.7") \ + /// #float("1e5") + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a float. + value: ToFloat, + ) -> f64 { + value.0 + } + + /// Checks if a float is not a number. + /// + /// In IEEE 754, more than one bit pattern represents a NaN. This function + /// returns `true` if the float is any of those bit patterns. + /// + /// ```example + /// #float.is-nan(0) \ + /// #float.is-nan(1) \ + /// #float.is-nan(float.nan) + /// ``` + #[func] + pub fn is_nan(self) -> bool { + f64::is_nan(self) + } + + /// Checks if a float is infinite. + /// + /// Floats can represent positive infinity and negative infinity. This + /// function returns `{true}` if the float is an infinity. + /// + /// ```example + /// #float.is-infinite(0) \ + /// #float.is-infinite(1) \ + /// #float.is-infinite(float.inf) + /// ``` + #[func] + pub fn is_infinite(self) -> bool { + f64::is_infinite(self) + } + + /// Calculates the sign of a floating point number. + /// + /// - If the number is positive (including `{+0.0}`), returns `{1.0}`. + /// - If the number is negative (including `{-0.0}`), returns `{-1.0}`. + /// - If the number is NaN, returns `{float.nan}`. + /// + /// ```example + /// #(5.0).signum() \ + /// #(-5.0).signum() \ + /// #(0.0).signum() \ + /// #float.nan.signum() + /// ``` + #[func] + pub fn signum(self) -> f64 { + f64::signum(self) + } + + /// Converts bytes to a float. + /// + /// ```example + /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ + /// #float.from-bytes(bytes((63, 240, 0, 0, 0, 0, 0, 0)), endian: "big") + /// ``` + #[func] + pub fn from_bytes( + /// The bytes that should be converted to a float. + /// + /// Must be of length exactly 8 so that the result fits into a 64-bit + /// float. + bytes: Bytes, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + ) -> StrResult<f64> { + // Convert slice to an array of length 8. + let buf: [u8; 8] = match bytes.as_ref().try_into() { + Ok(buffer) => buffer, + Err(_) => bail!("bytes must have a length of exactly 8"), + }; + + Ok(match endian { + Endianness::Little => f64::from_le_bytes(buf), + Endianness::Big => f64::from_be_bytes(buf), + }) + } + + /// Converts a float to bytes. + /// + /// ```example + /// #array(1.0.to-bytes(endian: "big")) \ + /// #array(1.0.to-bytes()) + /// ``` + #[func] + pub fn to_bytes( + self, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + ) -> Bytes { + match endian { + Endianness::Little => self.to_le_bytes(), + Endianness::Big => self.to_be_bytes(), + } + .as_slice() + .into() + } +} + +impl Repr for f64 { + fn repr(&self) -> EcoString { + repr::format_float(*self, None, true, "") + } +} + +/// A value that can be cast to a float. +pub struct ToFloat(f64); + +cast! { + ToFloat, + v: f64 => Self(v), + v: bool => Self(v as i64 as f64), + v: i64 => Self(v as f64), + v: Decimal => Self(f64::try_from(v).map_err(|_| eco_format!("invalid float: {}", v))?), + v: Ratio => Self(v.get()), + v: Str => Self( + parse_float(v.clone().into()) + .map_err(|_| eco_format!("invalid float: {}", v))? + ), +} + +fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> { + s.replace(repr::MINUS_SIGN, "-").parse() +} diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs new file mode 100644 index 00000000..1b40714b --- /dev/null +++ b/crates/typst-library/src/foundations/func.rs @@ -0,0 +1,541 @@ +#[doc(inline)] +pub use typst_macros::func; + +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use comemo::{Tracked, TrackedMut}; +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use typst_syntax::{ast, Span, SyntaxNode}; +use typst_utils::{singleton, LazyHash, Static}; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, + Selector, Type, Value, +}; + +/// A mapping from argument values to a return value. +/// +/// You can call a function by writing a comma-separated list of function +/// _arguments_ enclosed in parentheses directly after the function name. +/// Additionally, you can pass any number of trailing content blocks arguments +/// to a function _after_ the normal argument list. If the normal argument list +/// would become empty, it can be omitted. Typst supports positional and named +/// arguments. The former are identified by position and type, while the latter +/// are written as `name: value`. +/// +/// Within math mode, function calls have special behaviour. See the +/// [math documentation]($category/math) for more details. +/// +/// # Example +/// ```example +/// // Call a function. +/// #list([A], [B]) +/// +/// // Named arguments and trailing +/// // content blocks. +/// #enum(start: 2)[A][B] +/// +/// // Version without parentheses. +/// #list[A][B] +/// ``` +/// +/// Functions are a fundamental building block of Typst. Typst provides +/// functions for a variety of typesetting tasks. Moreover, the markup you write +/// is backed by functions and all styling happens through functions. This +/// reference lists all available functions and how you can use them. Please +/// also refer to the documentation about [set]($styling/#set-rules) and +/// [show]($styling/#show-rules) rules to learn about additional ways you can +/// work with functions in Typst. +/// +/// # Element functions +/// Some functions are associated with _elements_ like [headings]($heading) or +/// [tables]($table). When called, these create an element of their respective +/// kind. In contrast to normal functions, they can further be used in [set +/// rules]($styling/#set-rules), [show rules]($styling/#show-rules), and +/// [selectors]($selector). +/// +/// # Function scopes +/// Functions can hold related definitions in their own scope, similar to a +/// [module]($scripting/#modules). Examples of this are +/// [`assert.eq`]($assert.eq) or [`list.item`]($list.item). However, this +/// feature is currently only available for built-in functions. +/// +/// # Defining functions +/// You can define your own function with a [let binding]($scripting/#bindings) +/// that has a parameter list after the binding's name. The parameter list can +/// contain mandatory positional parameters, named parameters with default +/// values and [argument sinks]($arguments). +/// +/// The right-hand side of a function binding is the function body, which can be +/// a block or any other expression. It defines the function's return value and +/// can depend on the parameters. If the function body is a [code +/// block]($scripting/#blocks), the return value is the result of joining the +/// values of each expression in the block. +/// +/// Within a function body, the `return` keyword can be used to exit early and +/// optionally specify a return value. If no explicit return value is given, the +/// body evaluates to the result of joining all expressions preceding the +/// `return`. +/// +/// Functions that don't return any meaningful value return [`none`] instead. +/// The return type of such functions is not explicitly specified in the +/// documentation. (An example of this is [`array.push`]). +/// +/// ```example +/// #let alert(body, fill: red) = { +/// set text(white) +/// set align(center) +/// rect( +/// fill: fill, +/// inset: 8pt, +/// radius: 4pt, +/// [*Warning:\ #body*], +/// ) +/// } +/// +/// #alert[ +/// Danger is imminent! +/// ] +/// +/// #alert(fill: blue)[ +/// KEEP OFF TRACKS +/// ] +/// ``` +/// +/// # Importing functions +/// Functions can be imported from one file ([`module`]($scripting/#modules)) into +/// another using `{import}`. For example, assume that we have defined the `alert` +/// function from the previous example in a file called `foo.typ`. We can import +/// it into another file by writing `{import "foo.typ": alert}`. +/// +/// # Unnamed functions { #unnamed } +/// You can also created an unnamed function without creating a binding by +/// specifying a parameter list followed by `=>` and the function body. If your +/// function has just one parameter, the parentheses around the parameter list +/// are optional. Unnamed functions are mainly useful for show rules, but also +/// for settable properties that take functions like the page function's +/// [`footer`]($page.footer) property. +/// +/// ```example +/// #show "once?": it => [#it #it] +/// once? +/// ``` +/// +/// # Note on function purity +/// In Typst, all functions are _pure._ This means that for the same +/// arguments, they always return the same result. They cannot "remember" things to +/// produce another value when they are called a second time. +/// +/// The only exception are built-in methods like +/// [`array.push(value)`]($array.push). These can modify the values they are +/// called on. +#[ty(scope, cast, name = "function")] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Func { + /// The internal representation. + repr: Repr, + /// The span with which errors are reported when this function is called. + span: Span, +} + +/// The different kinds of function representations. +#[derive(Clone, PartialEq, Hash)] +enum Repr { + /// A native Rust function. + Native(Static<NativeFuncData>), + /// A function for an element. + Element(Element), + /// A user-defined closure. + Closure(Arc<LazyHash<Closure>>), + /// A nested function with pre-applied arguments. + With(Arc<(Func, Args)>), +} + +impl Func { + /// The function's name (e.g. `min`). + /// + /// Returns `None` if this is an anonymous closure. + pub fn name(&self) -> Option<&str> { + match &self.repr { + Repr::Native(native) => Some(native.name), + Repr::Element(elem) => Some(elem.name()), + Repr::Closure(closure) => closure.name(), + Repr::With(with) => with.0.name(), + } + } + + /// The function's title case name, for use in documentation (e.g. `Minimum`). + /// + /// Returns `None` if this is a closure. + pub fn title(&self) -> Option<&'static str> { + match &self.repr { + Repr::Native(native) => Some(native.title), + Repr::Element(elem) => Some(elem.title()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.title(), + } + } + + /// Documentation for the function (as Markdown). + pub fn docs(&self) -> Option<&'static str> { + match &self.repr { + Repr::Native(native) => Some(native.docs), + Repr::Element(elem) => Some(elem.docs()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.docs(), + } + } + + /// Whether the function is known to be contextual. + pub fn contextual(&self) -> Option<bool> { + match &self.repr { + Repr::Native(native) => Some(native.contextual), + _ => None, + } + } + + /// Get details about this function's parameters if available. + pub fn params(&self) -> Option<&'static [ParamInfo]> { + match &self.repr { + Repr::Native(native) => Some(&native.0.params), + Repr::Element(elem) => Some(elem.params()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.params(), + } + } + + /// Get the parameter info for a parameter with the given name if it exist. + pub fn param(&self, name: &str) -> Option<&'static ParamInfo> { + self.params()?.iter().find(|param| param.name == name) + } + + /// Get details about the function's return type. + pub fn returns(&self) -> Option<&'static CastInfo> { + match &self.repr { + Repr::Native(native) => Some(&native.0.returns), + Repr::Element(_) => { + Some(singleton!(CastInfo, CastInfo::Type(Type::of::<Content>()))) + } + Repr::Closure(_) => None, + Repr::With(with) => with.0.returns(), + } + } + + /// Search keywords for the function. + pub fn keywords(&self) -> &'static [&'static str] { + match &self.repr { + Repr::Native(native) => native.keywords, + Repr::Element(elem) => elem.keywords(), + Repr::Closure(_) => &[], + Repr::With(with) => with.0.keywords(), + } + } + + /// The function's associated scope of sub-definition. + pub fn scope(&self) -> Option<&'static Scope> { + match &self.repr { + Repr::Native(native) => Some(&native.0.scope), + Repr::Element(elem) => Some(elem.scope()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.scope(), + } + } + + /// Get a field from this function's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + let scope = + self.scope().ok_or("cannot access fields on user-defined functions")?; + match scope.get(field) { + Some(field) => Ok(field), + None => match self.name() { + Some(name) => bail!("function `{name}` does not contain field `{field}`"), + None => bail!("function does not contain field `{field}`"), + }, + } + } + + /// Extract the element function, if it is one. + pub fn element(&self) -> Option<Element> { + match self.repr { + Repr::Element(func) => Some(func), + _ => None, + } + } + + /// Call the function with the given context and arguments. + pub fn call<A: IntoArgs>( + &self, + engine: &mut Engine, + context: Tracked<Context>, + args: A, + ) -> SourceResult<Value> { + self.call_impl(engine, context, args.into_args(self.span)) + } + + /// Non-generic implementation of `call`. + #[typst_macros::time(name = "func call", span = self.span())] + fn call_impl( + &self, + engine: &mut Engine, + context: Tracked<Context>, + mut args: Args, + ) -> SourceResult<Value> { + match &self.repr { + Repr::Native(native) => { + let value = (native.function)(engine, context, &mut args)?; + args.finish()?; + Ok(value) + } + Repr::Element(func) => { + let value = func.construct(engine, &mut args)?; + args.finish()?; + Ok(Value::Content(value)) + } + Repr::Closure(closure) => (engine.routines.eval_closure)( + self, + closure, + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + context, + args, + ), + Repr::With(with) => { + args.items = with.1.items.iter().cloned().chain(args.items).collect(); + with.0.call(engine, context, args) + } + } + } + + /// The function's span. + pub fn span(&self) -> Span { + self.span + } + + /// Attach a span to this function if it doesn't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } +} + +#[scope] +impl Func { + /// Returns a new function that has the given arguments pre-applied. + #[func] + pub fn with( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The arguments to apply to the function. + #[external] + #[variadic] + arguments: Vec<Value>, + ) -> Func { + let span = self.span; + Self { + repr: Repr::With(Arc::new((self, args.take()))), + span, + } + } + + /// Returns a selector that filters for elements belonging to this function + /// whose fields have the values of the given arguments. + /// + /// ```example + /// #show heading.where(level: 2): set text(blue) + /// = Section + /// == Subsection + /// === Sub-subsection + /// ``` + #[func] + pub fn where_( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The fields to filter for. + #[variadic] + #[external] + fields: Vec<Value>, + ) -> StrResult<Selector> { + let fields = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + + let element = self + .element() + .ok_or("`where()` can only be called on element functions")?; + + let fields = fields + .into_iter() + .map(|(key, value)| { + element.field_id(&key).map(|id| (id, value)).ok_or_else(|| { + eco_format!( + "element `{}` does not have field `{}`", + element.name(), + key + ) + }) + }) + .collect::<StrResult<smallvec::SmallVec<_>>>()?; + + Ok(element.where_(fields)) + } +} + +impl Debug for Func { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Func({})", self.name().unwrap_or("..")) + } +} + +impl repr::Repr for Func { + fn repr(&self) -> EcoString { + match self.name() { + Some(name) => name.into(), + None => "(..) => ..".into(), + } + } +} + +impl PartialEq for Func { + fn eq(&self, other: &Self) -> bool { + self.repr == other.repr + } +} + +impl PartialEq<&NativeFuncData> for Func { + fn eq(&self, other: &&NativeFuncData) -> bool { + match &self.repr { + Repr::Native(native) => native.function == other.function, + _ => false, + } + } +} + +impl From<Repr> for Func { + fn from(repr: Repr) -> Self { + Self { repr, span: Span::detached() } + } +} + +impl From<Element> for Func { + fn from(func: Element) -> Self { + Repr::Element(func).into() + } +} + +/// A Typst function that is defined by a native Rust type that shadows a +/// native Rust function. +pub trait NativeFunc { + /// Get the function for the native Rust type. + fn func() -> Func { + Func::from(Self::data()) + } + + /// Get the function data for the native Rust type. + fn data() -> &'static NativeFuncData; +} + +/// Defines a native function. +#[derive(Debug)] +pub struct NativeFuncData { + /// Invokes the function from Typst. + pub function: fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value>, + /// The function's normal name (e.g. `align`), as exposed to Typst. + pub name: &'static str, + /// The function's title case name (e.g. `Align`). + pub title: &'static str, + /// The documentation for this function as a string. + pub docs: &'static str, + /// A list of alternate search terms for this function. + pub keywords: &'static [&'static str], + /// Whether this function makes use of context. + pub contextual: bool, + pub scope: Lazy<Scope>, + /// A list of parameter information for each parameter. + pub params: Lazy<Vec<ParamInfo>>, + /// Information about the return value of this function. + pub returns: Lazy<CastInfo>, +} + +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() + } +} + +cast! { + &'static NativeFuncData, + self => Func::from(self).into_value(), +} + +/// Describes a function parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Describe what values this parameter accepts. + pub input: CastInfo, + /// Creates an instance of the parameter's default value. + pub default: Option<fn() -> Value>, + /// Is the parameter positional? + pub positional: bool, + /// Is the parameter named? + /// + /// Can be true even if `positional` is true if the parameter can be given + /// in both variants. + pub named: bool, + /// Can the parameter be given any number of times? + pub variadic: bool, + /// Is the parameter required? + pub required: bool, + /// Is the parameter settable with a set rule? + pub settable: bool, +} + +/// A user-defined closure. +#[derive(Debug, Hash)] +pub struct Closure { + /// The closure's syntax node. Must be either castable to `ast::Closure` or + /// `ast::Expr`. In the latter case, this is a synthesized closure without + /// any parameters (used by `context` expressions). + pub node: SyntaxNode, + /// Default values of named parameters. + pub defaults: Vec<Value>, + /// Captured values from outer scopes. + pub captured: Scope, + /// The number of positional parameters in the closure. + pub num_pos_params: usize, +} + +impl Closure { + /// The name of the closure. + pub fn name(&self) -> Option<&str> { + self.node.cast::<ast::Closure>()?.name().map(|ident| ident.as_str()) + } +} + +impl From<Closure> for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(LazyHash::new(closure))).into() + } +} + +cast! { + Closure, + self => Value::Func(self.into()), +} diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs new file mode 100644 index 00000000..e936353c --- /dev/null +++ b/crates/typst-library/src/foundations/int.rs @@ -0,0 +1,478 @@ +use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value, +}; + +/// A whole number. +/// +/// The number can be negative, zero, or positive. As Typst uses 64 bits to +/// store integers, integers cannot be smaller than `{-9223372036854775808}` or +/// larger than `{9223372036854775807}`. +/// +/// The number can also be specified as hexadecimal, octal, or binary by +/// starting it with a zero followed by either `x`, `o`, or `b`. +/// +/// You can convert a value to an integer with this type's constructor. +/// +/// # Example +/// ```example +/// #(1 + 2) \ +/// #(2 - 5) \ +/// #(3 + 4 < 8) +/// +/// #0xff \ +/// #0o10 \ +/// #0b1001 +/// ``` +#[ty(scope, cast, name = "int", title = "Integer")] +type i64; + +#[scope] +impl i64 { + /// Converts a value to an integer. Raises an error if there is an attempt + /// to produce an integer larger than the maximum 64-bit signed integer + /// or smaller than the minimum 64-bit signed integer. + /// + /// - Booleans are converted to `0` or `1`. + /// - Floats and decimals are truncated to the next 64-bit integer. + /// - Strings are parsed in base 10. + /// + /// ```example + /// #int(false) \ + /// #int(true) \ + /// #int(2.7) \ + /// #int(decimal("3.8")) \ + /// #(int("27") + int("4")) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an integer. + value: ToInt, + ) -> i64 { + value.0 + } + + /// Calculates the sign of an integer. + /// + /// - If the number is positive, returns `{1}`. + /// - If the number is negative, returns `{-1}`. + /// - If the number is zero, returns `{0}`. + /// + /// ```example + /// #(5).signum() \ + /// #(-5).signum() \ + /// #(0).signum() + /// ``` + #[func] + pub fn signum(self) -> i64 { + i64::signum(self) + } + + /// Calculates the bitwise NOT of an integer. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. + /// + /// ```example + /// #4.bit-not() \ + /// #(-1).bit-not() + /// ``` + #[func(title = "Bitwise NOT")] + pub fn bit_not(self) -> i64 { + !self + } + + /// Calculates the bitwise AND between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #128.bit-and(192) + /// ``` + #[func(title = "Bitwise AND")] + pub fn bit_and( + self, + /// The right-hand operand of the bitwise AND. + rhs: i64, + ) -> i64 { + self & rhs + } + + /// Calculates the bitwise OR between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #64.bit-or(32) + /// ``` + #[func(title = "Bitwise OR")] + pub fn bit_or( + self, + /// The right-hand operand of the bitwise OR. + rhs: i64, + ) -> i64 { + self | rhs + } + + /// Calculates the bitwise XOR between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #64.bit-xor(96) + /// ``` + #[func(title = "Bitwise XOR")] + pub fn bit_xor( + self, + /// The right-hand operand of the bitwise XOR. + rhs: i64, + ) -> i64 { + self ^ rhs + } + + /// Shifts the operand's bits to the left by the specified amount. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. An error will occur if the result is too large to + /// fit in a 64-bit integer. + /// + /// ```example + /// #33.bit-lshift(2) \ + /// #(-1).bit-lshift(3) + /// ``` + #[func(title = "Bitwise Left Shift")] + pub fn bit_lshift( + self, + /// The amount of bits to shift. Must not be negative. + shift: u32, + ) -> StrResult<i64> { + Ok(self.checked_shl(shift).ok_or("the result is too large")?) + } + + /// Shifts the operand's bits to the right by the specified amount. + /// Performs an arithmetic shift by default (extends the sign bit to the left, + /// such that negative numbers stay negative), but that can be changed by the + /// `logical` parameter. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. + /// + /// ```example + /// #64.bit-rshift(2) \ + /// #(-8).bit-rshift(2) \ + /// #(-8).bit-rshift(2, logical: true) + /// ``` + #[func(title = "Bitwise Right Shift")] + pub fn bit_rshift( + self, + /// The amount of bits to shift. Must not be negative. + /// + /// Shifts larger than 63 are allowed and will cause the return value to + /// saturate. For non-negative numbers, the return value saturates at + /// `{0}`, while, for negative numbers, it saturates at `{-1}` if + /// `logical` is set to `{false}`, or `{0}` if it is `{true}`. This + /// behavior is consistent with just applying this operation multiple + /// times. Therefore, the shift will always succeed. + shift: u32, + /// Toggles whether a logical (unsigned) right shift should be performed + /// instead of arithmetic right shift. + /// If this is `{true}`, negative operands will not preserve their sign + /// bit, and bits which appear to the left after the shift will be + /// `{0}`. This parameter has no effect on non-negative operands. + #[named] + #[default(false)] + logical: bool, + ) -> i64 { + if logical { + if shift >= u64::BITS { + // Excessive logical right shift would be equivalent to setting + // all bits to zero. Using `.min(63)` is not enough for logical + // right shift, since `-1 >> 63` returns 1, whereas + // `calc.bit-rshift(-1, 64)` should return the same as + // `(-1 >> 63) >> 1`, which is zero. + 0 + } else { + // Here we reinterpret the signed integer's bits as unsigned to + // perform logical right shift, and then reinterpret back as signed. + // This is valid as, according to the Rust reference, casting between + // two integers of same size (i64 <-> u64) is a no-op (two's complement + // is used). + // Reference: + // https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#numeric-cast + ((self as u64) >> shift) as i64 + } + } else { + // Saturate at -1 (negative) or 0 (otherwise) on excessive arithmetic + // right shift. Shifting those numbers any further does not change + // them, so it is consistent. + let shift = shift.min(i64::BITS - 1); + self >> shift + } + } + + /// Converts bytes to an integer. + /// + /// ```example + /// #int.from-bytes(bytes((0, 0, 0, 0, 0, 0, 0, 1))) \ + /// #int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "big") + /// ``` + #[func] + pub fn from_bytes( + /// The bytes that should be converted to an integer. + /// + /// Must be of length at most 8 so that the result fits into a 64-bit + /// signed integer. + bytes: Bytes, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// Whether the bytes should be treated as a signed integer. If this is + /// `{true}` and the most significant bit is set, the resulting number + /// will negative. + #[named] + #[default(true)] + signed: bool, + ) -> StrResult<i64> { + let len = bytes.len(); + if len == 0 { + return Ok(0); + } else if len > 8 { + bail!("too many bytes to convert to a 64 bit number"); + } + + // `decimal` will hold the part of the buffer that should be filled with + // the input bytes, `rest` will remain as is or be filled with 0xFF for + // negative numbers if signed is true. + // + // – big-endian: `decimal` will be the rightmost bytes of the buffer. + // - little-endian: `decimal` will be the leftmost bytes of the buffer. + let mut buf = [0u8; 8]; + let (rest, decimal) = match endian { + Endianness::Big => buf.split_at_mut(8 - len), + Endianness::Little => { + let (first, second) = buf.split_at_mut(len); + (second, first) + } + }; + + decimal.copy_from_slice(bytes.as_ref()); + + // Perform sign-extension if necessary. + if signed { + let most_significant_byte = match endian { + Endianness::Big => decimal[0], + Endianness::Little => decimal[len - 1], + }; + + if most_significant_byte & 0b1000_0000 != 0 { + rest.fill(0xFF); + } + } + + Ok(match endian { + Endianness::Big => i64::from_be_bytes(buf), + Endianness::Little => i64::from_le_bytes(buf), + }) + } + + /// Converts an integer to bytes. + /// + /// ```example + /// #array(10000.to-bytes(endian: "big")) \ + /// #array(10000.to-bytes(size: 4)) + /// ``` + #[func] + pub fn to_bytes( + self, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// The size in bytes of the resulting bytes (must be at least zero). If + /// the integer is too large to fit in the specified size, the + /// conversion will truncate the remaining bytes based on the + /// endianness. To keep the same resulting value, if the endianness is + /// big-endian, the truncation will happen at the rightmost bytes. + /// Otherwise, if the endianness is little-endian, the truncation will + /// happen at the leftmost bytes. + /// + /// Be aware that if the integer is negative and the size is not enough + /// to make the number fit, when passing the resulting bytes to + /// `int.from-bytes`, the resulting number might be positive, as the + /// most significant bit might not be set to 1. + #[named] + #[default(8)] + size: usize, + ) -> Bytes { + let array = match endian { + Endianness::Big => self.to_be_bytes(), + Endianness::Little => self.to_le_bytes(), + }; + + let mut buf = vec![0u8; size]; + match endian { + Endianness::Big => { + // Copy the bytes from the array to the buffer, starting from + // the end of the buffer. + let buf_start = size.saturating_sub(8); + let array_start = 8usize.saturating_sub(size); + buf[buf_start..].copy_from_slice(&array[array_start..]) + } + Endianness::Little => { + // Copy the bytes from the array to the buffer, starting from + // the beginning of the buffer. + let end = size.min(8); + buf[..end].copy_from_slice(&array[..end]) + } + } + + Bytes::from(buf) + } +} + +impl Repr for i64 { + fn repr(&self) -> EcoString { + eco_format!("{:?}", self) + } +} + +/// Represents the byte order used for converting integers and floats to bytes +/// and vice versa. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Endianness { + /// Big-endian byte order: The highest-value byte is at the beginning of the + /// bytes. + Big, + /// Little-endian byte order: The lowest-value byte is at the beginning of + /// the bytes. + Little, +} + +/// A value that can be cast to an integer. +pub struct ToInt(i64); + +cast! { + ToInt, + v: i64 => Self(v), + v: bool => Self(v as i64), + v: f64 => Self(convert_float_to_int(v)?), + v: Decimal => Self(i64::try_from(v).map_err(|_| eco_format!("number too large"))?), + v: Str => Self(parse_int(&v).map_err(|_| eco_format!("invalid integer: {}", v))?), +} + +pub fn convert_float_to_int(f: f64) -> StrResult<i64> { + if f <= i64::MIN as f64 - 1.0 || f >= i64::MAX as f64 + 1.0 { + Err(eco_format!("number too large")) + } else { + Ok(f as i64) + } +} + +fn parse_int(mut s: &str) -> Result<i64, ParseIntError> { + let mut sign = 1; + if let Some(rest) = s.strip_prefix('-').or_else(|| s.strip_prefix(repr::MINUS_SIGN)) { + sign = -1; + s = rest; + } + if sign == -1 && s == "9223372036854775808" { + return Ok(i64::MIN); + } + Ok(sign * s.parse::<i64>()?) +} + +macro_rules! signed_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as _), + v: i64 => v.try_into().map_err(|_| "number too large")?, + })* + } +} + +macro_rules! unsigned_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => { + #[allow(irrefutable_let_patterns)] + if let Ok(int) = i64::try_from(self) { + Value::Int(int) + } else { + // Some u64 are too large to be cast as i64 + // In that case, we accept that there may be a + // precision loss, and use a floating point number + Value::Float(self as _) + } + }, + v: i64 => v.try_into().map_err(|_| { + if v < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, + })* + } +} + +signed_int! { i8 i16 i32 isize } +unsigned_int! { u8 u16 u32 u64 usize } + +cast! { + NonZeroI64, + self => Value::Int(self.get() as _), + v: i64 => v.try_into() + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, +} + +cast! { + NonZeroIsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: isize| v.try_into()) + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, +} + +cast! { + NonZeroU64, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: u64| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} + +cast! { + NonZeroUsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: usize| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} diff --git a/crates/typst-library/src/foundations/label.rs b/crates/typst-library/src/foundations/label.rs new file mode 100644 index 00000000..726958df --- /dev/null +++ b/crates/typst-library/src/foundations/label.rs @@ -0,0 +1,93 @@ +use ecow::{eco_format, EcoString}; +use typst_utils::PicoStr; + +use crate::foundations::{func, scope, ty, Repr}; + +/// A label for an element. +/// +/// Inserting a label into content attaches it to the closest preceding element +/// that is not a space. The preceding element must be in the same scope as the +/// label, which means that `[Hello #[<label>]]`, for instance, wouldn't work. +/// +/// A labelled element can be [referenced]($ref), [queried]($query) for, and +/// [styled]($styling) through its label. +/// +/// Once constructed, you can get the name of a label using +/// [`str`]($str/#constructor). +/// +/// # Example +/// ```example +/// #show <a>: set text(blue) +/// #show label("b"): set text(red) +/// +/// = Heading <a> +/// *Strong* #label("b") +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: You can create a label by enclosing +/// its name in angle brackets. This works both in markup and code. A label's +/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. +/// +/// Note that there is a syntactical difference when using the dedicated syntax +/// for this function. In the code below, the `[<a>]` terminates the heading and +/// thus attaches to the heading itself, whereas the `[#label("b")]` is part of +/// the heading and thus attaches to the heading's text. +/// +/// ```typ +/// // Equivalent to `#heading[Introduction] <a>`. +/// = Introduction <a> +/// +/// // Equivalent to `#heading[Conclusion #label("b")]`. +/// = Conclusion #label("b") +/// ``` +/// +/// Currently, labels can only be attached to elements in markup mode, not in +/// code mode. This might change in the future. +#[ty(scope, cast)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(PicoStr); + +impl Label { + /// Creates a label from a string, interning it. + pub fn new(name: impl Into<PicoStr>) -> Self { + Self(name.into()) + } + + /// Resolves the label to a string. + pub fn as_str(&self) -> &'static str { + self.0.resolve() + } + + /// Turns this label into its inner interned string. + pub fn into_inner(self) -> PicoStr { + self.0 + } +} + +#[scope] +impl Label { + /// Creates a label from a string. + #[func(constructor)] + pub fn construct( + /// The name of the label. + name: PicoStr, + ) -> Label { + Self(name) + } +} + +impl Repr for Label { + fn repr(&self) -> EcoString { + eco_format!("<{}>", self.as_str()) + } +} + +impl From<Label> for PicoStr { + fn from(value: Label) -> Self { + value.into_inner() + } +} + +/// Indicates that an element cannot be labelled. +pub trait Unlabellable {} diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs new file mode 100644 index 00000000..a6d6c253 --- /dev/null +++ b/crates/typst-library/src/foundations/mod.rs @@ -0,0 +1,302 @@ +//! Foundational types and functions. + +pub mod calc; +pub mod ops; +pub mod repr; +pub mod sys; + +mod args; +mod array; +mod auto; +mod bool; +mod bytes; +mod cast; +mod content; +mod context; +mod datetime; +mod decimal; +mod dict; +mod duration; +mod element; +mod fields; +mod float; +mod func; +mod int; +mod label; +mod module; +mod none; +mod plugin; +mod scope; +mod selector; +mod str; +mod styles; +mod symbol; +mod ty; +mod value; +mod version; + +pub use self::args::*; +pub use self::array::*; +pub use self::auto::*; +pub use self::bytes::*; +pub use self::cast::*; +pub use self::content::*; +pub use self::context::*; +pub use self::datetime::*; +pub use self::decimal::*; +pub use self::dict::*; +pub use self::duration::*; +pub use self::element::*; +pub use self::fields::*; +pub use self::float::*; +pub use self::func::*; +pub use self::int::*; +pub use self::label::*; +pub use self::module::*; +pub use self::none::*; +pub use self::plugin::*; +pub use self::repr::Repr; +pub use self::scope::*; +pub use self::selector::*; +pub use self::str::*; +pub use self::styles::*; +pub use self::symbol::*; +pub use self::ty::*; +pub use self::value::*; +pub use self::version::*; +pub use typst_macros::{scope, ty}; + +#[rustfmt::skip] +#[doc(hidden)] +pub use { + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, +}; + +use ecow::EcoString; +use typst_syntax::Spanned; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::routines::EvalMode; + +/// Foundational types and functions. +/// +/// Here, you'll find documentation for basic data types like [integers]($int) +/// and [strings]($str) as well as details about core computational functions. +#[category] +pub static FOUNDATIONS: Category; + +/// Hook up all `foundations` definitions. +pub(super) fn define(global: &mut Scope, inputs: Dict) { + global.category(FOUNDATIONS); + global.define_type::<bool>(); + global.define_type::<i64>(); + global.define_type::<f64>(); + global.define_type::<Str>(); + global.define_type::<Label>(); + global.define_type::<Bytes>(); + global.define_type::<Content>(); + global.define_type::<Array>(); + global.define_type::<Dict>(); + global.define_type::<Func>(); + global.define_type::<Args>(); + global.define_type::<Type>(); + global.define_type::<Module>(); + global.define_type::<Regex>(); + global.define_type::<Selector>(); + global.define_type::<Datetime>(); + global.define_type::<Decimal>(); + global.define_type::<Symbol>(); + global.define_type::<Duration>(); + global.define_type::<Version>(); + global.define_type::<Plugin>(); + global.define_func::<repr::repr>(); + global.define_func::<panic>(); + global.define_func::<assert>(); + global.define_func::<eval>(); + global.define_func::<style>(); + global.define_module(calc::module()); + global.define_module(sys::module(inputs)); +} + +/// Fails with an error. +/// +/// Arguments are displayed to the user (not rendered in the document) as +/// strings, converting with `repr` if necessary. +/// +/// # Example +/// The code below produces the error `panicked with: "this is wrong"`. +/// ```typ +/// #panic("this is wrong") +/// ``` +#[func(keywords = ["error"])] +pub fn panic( + /// The values to panic with and display to the user. + #[variadic] + values: Vec<Value>, +) -> StrResult<Never> { + let mut msg = EcoString::from("panicked"); + if !values.is_empty() { + msg.push_str(" with: "); + for (i, value) in values.iter().enumerate() { + if i > 0 { + msg.push_str(", "); + } + msg.push_str(&value.repr()); + } + } + Err(msg) +} + +/// Ensures that a condition is fulfilled. +/// +/// Fails with an error if the condition is not fulfilled. Does not +/// produce any output in the document. +/// +/// If you wish to test equality between two values, see +/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne). +/// +/// # Example +/// ```typ +/// #assert(1 < 2, message: "math broke") +/// ``` +#[func(scope)] +pub fn assert( + /// The condition that must be true for the assertion to pass. + condition: bool, + /// The error message when the assertion fails. + #[named] + message: Option<EcoString>, +) -> StrResult<NoneValue> { + if !condition { + if let Some(message) = message { + bail!("assertion failed: {message}"); + } else { + bail!("assertion failed"); + } + } + Ok(NoneValue) +} + +#[scope] +impl assert { + /// Ensures that two values are equal. + /// + /// Fails with an error if the first value is not equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.eq(10, 10) + /// ``` + #[func(title = "Assert Equal")] + pub fn eq( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left != right { + if let Some(message) = message { + bail!("equality assertion failed: {message}"); + } else { + bail!( + "equality assertion failed: value {} was not equal to {}", + left.repr(), + right.repr() + ); + } + } + Ok(NoneValue) + } + + /// Ensures that two values are not equal. + /// + /// Fails with an error if the first value is equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.ne(3, 4) + /// ``` + #[func(title = "Assert Not Equal")] + pub fn ne( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left == right { + if let Some(message) = message { + bail!("inequality assertion failed: {message}"); + } else { + bail!( + "inequality assertion failed: value {} was equal to {}", + left.repr(), + right.repr() + ); + } + } + Ok(NoneValue) + } +} + +/// Evaluates a string as Typst code. +/// +/// This function should only be used as a last resort. +/// +/// # Example +/// ```example +/// #eval("1 + 1") \ +/// #eval("(1, 2, 3, 4)").len() \ +/// #eval("*Markup!*", mode: "markup") \ +/// ``` +#[func(title = "Evaluate")] +pub fn eval( + /// The engine. + engine: &mut Engine, + /// A string of Typst code to evaluate. + source: Spanned<String>, + /// The [syntactical mode]($reference/syntax/#modes) in which the string is + /// parsed. + /// + /// ```example + /// #eval("= Heading", mode: "markup") + /// #eval("1_2^3", mode: "math") + /// ``` + #[named] + #[default(EvalMode::Code)] + mode: EvalMode, + /// A scope of definitions that are made available. + /// + /// ```example + /// #eval("x + 1", scope: (x: 2)) \ + /// #eval( + /// "abc/xyz", + /// mode: "math", + /// scope: ( + /// abc: $a + b + c$, + /// xyz: $x + y + z$, + /// ), + /// ) + /// ``` + #[named] + #[default] + scope: Dict, +) -> SourceResult<Value> { + let Spanned { v: text, span } = source; + let dict = scope; + let mut scope = Scope::new(); + for (key, value) in dict { + scope.define_spanned(key, value, span); + } + (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) +} diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs new file mode 100644 index 00000000..a476d6af --- /dev/null +++ b/crates/typst-library/src/foundations/module.rs @@ -0,0 +1,142 @@ +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use typst_syntax::FileId; + +use crate::diag::StrResult; +use crate::foundations::{repr, ty, Content, Scope, Value}; + +/// An evaluated module, either built-in or resulting from a file. +/// +/// You can access definitions from the module using +/// [field access notation]($scripting/#fields) and interact with it using the +/// [import and include syntaxes]($scripting/#modules). Alternatively, it is +/// possible to convert a module to a dictionary, and therefore access its +/// contents dynamically, using the +/// [dictionary constructor]($dictionary/#constructor). +/// +/// # Example +/// ```example +/// <<< #import "utils.typ" +/// <<< #utils.add(2, 5) +/// +/// <<< #import utils: sub +/// <<< #sub(1, 4) +/// >>> #7 +/// >>> +/// >>> #(-3) +/// ``` +#[ty(cast)] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Module { + /// The module's name. + name: EcoString, + /// The reference-counted inner fields. + inner: Arc<Repr>, +} + +/// The internal representation. +#[derive(Debug, Clone, Hash)] +struct Repr { + /// The top-level definitions that were bound in this module. + scope: Scope, + /// The module's layoutable contents. + content: Content, + /// The id of the file which defines the module, if any. + file_id: Option<FileId>, +} + +impl Module { + /// Create a new module. + pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self { + Self { + name: name.into(), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), + } + } + + /// Update the module's name. + pub fn with_name(mut self, name: impl Into<EcoString>) -> Self { + self.name = name.into(); + self + } + + /// Update the module's scope. + pub fn with_scope(mut self, scope: Scope) -> Self { + Arc::make_mut(&mut self.inner).scope = scope; + self + } + + /// Update the module's content. + pub fn with_content(mut self, content: Content) -> Self { + Arc::make_mut(&mut self.inner).content = content; + self + } + + /// Update the module's file id. + pub fn with_file_id(mut self, file_id: FileId) -> Self { + Arc::make_mut(&mut self.inner).file_id = Some(file_id); + self + } + + /// Get the module's name. + pub fn name(&self) -> &EcoString { + &self.name + } + + /// Access the module's scope. + pub fn scope(&self) -> &Scope { + &self.inner.scope + } + + /// Access the module's file id. + /// + /// Some modules are not associated with a file, like the built-in modules. + pub fn file_id(&self) -> Option<FileId> { + self.inner.file_id + } + + /// Access the module's scope, mutably. + pub fn scope_mut(&mut self) -> &mut Scope { + &mut Arc::make_mut(&mut self.inner).scope + } + + /// Try to access a definition in the module. + pub fn field(&self, name: &str) -> StrResult<&Value> { + self.scope().get(name).ok_or_else(|| { + eco_format!("module `{}` does not contain `{name}`", self.name()) + }) + } + + /// Extract the module's content. + pub fn content(self) -> Content { + match Arc::try_unwrap(self.inner) { + Ok(repr) => repr.content, + Err(arc) => arc.content.clone(), + } + } +} + +impl Debug for Module { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Module") + .field("name", &self.name) + .field("scope", &self.inner.scope) + .field("content", &self.inner.content) + .finish() + } +} + +impl repr::Repr for Module { + fn repr(&self) -> EcoString { + eco_format!("<module {}>", self.name()) + } +} + +impl PartialEq for Module { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && Arc::ptr_eq(&self.inner, &other.inner) + } +} diff --git a/crates/typst-library/src/foundations/none.rs b/crates/typst-library/src/foundations/none.rs new file mode 100644 index 00000000..d376c0c5 --- /dev/null +++ b/crates/typst-library/src/foundations/none.rs @@ -0,0 +1,114 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; +use serde::{Serialize, Serializer}; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value, +}; + +/// A value that indicates the absence of any other value. +/// +/// The none type has exactly one value: `{none}`. +/// +/// When inserted into the document, it is not visible. This is also the value +/// that is produced by empty code blocks. It can be +/// [joined]($scripting/#blocks) with any value, yielding the other value. +/// +/// # Example +/// ```example +/// Not visible: #none +/// ``` +#[ty(cast, name = "none")] +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NoneValue; + +impl Reflect for NoneValue { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::None) + } +} + +impl IntoValue for NoneValue { + fn into_value(self) -> Value { + Value::None + } +} + +impl FromValue for NoneValue { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::None => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Debug for NoneValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("None") + } +} + +impl Repr for NoneValue { + fn repr(&self) -> EcoString { + "none".into() + } +} + +impl Serialize for NoneValue { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_none() + } +} + +cast! { + (), + self => Value::None, + _: NoneValue => (), +} + +impl<T: Reflect> Reflect for Option<T> { + fn input() -> CastInfo { + T::input() + NoneValue::input() + } + + fn output() -> CastInfo { + T::output() + NoneValue::output() + } + + fn castable(value: &Value) -> bool { + NoneValue::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Option<T> { + fn into_value(self) -> Value { + match self { + Some(v) => v.into_value(), + None => Value::None, + } + } +} + +impl<T: FromValue> FromValue for Option<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::None => Ok(None), + v if T::castable(&v) => Ok(Some(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs new file mode 100644 index 00000000..ba36137f --- /dev/null +++ b/crates/typst-library/src/foundations/ops.rs @@ -0,0 +1,583 @@ +//! Operations on values. + +use std::cmp::Ordering; + +use ecow::eco_format; +use typst_utils::Numeric; + +use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; +use crate::layout::{Alignment, Length, Rel}; +use crate::text::TextElem; +use crate::visualize::Stroke; + +/// Bail with a type mismatch error. +macro_rules! mismatch { + ($fmt:expr, $($value:expr),* $(,)?) => { + return Err(eco_format!($fmt, $($value.ty()),*).into()) + }; +} + +/// Join a value with another value. +pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Bytes(a), Bytes(b)) => Bytes(a + b), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + // Type compatibility. + (Type(a), Str(b)) => Str(format_str!("{a}{b}")), + (Str(a), Type(b)) => Str(format_str!("{a}{b}")), + + (a, b) => mismatch!("cannot join {} with {}", a, b), + }) +} + +/// Apply the unary plus operator to a value. +pub fn pos(value: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match value { + Int(v) => Int(v), + Float(v) => Float(v), + Decimal(v) => Decimal(v), + Length(v) => Length(v), + Angle(v) => Angle(v), + Ratio(v) => Ratio(v), + Relative(v) => Relative(v), + Fraction(v) => Fraction(v), + Symbol(_) | Str(_) | Bytes(_) | Content(_) | Array(_) | Dict(_) | Datetime(_) => { + mismatch!("cannot apply unary '+' to {}", value) + } + Dyn(d) => { + if d.is::<Alignment>() { + mismatch!("cannot apply unary '+' to {}", d) + } else { + mismatch!("cannot apply '+' to {}", d) + } + } + v => mismatch!("cannot apply '+' to {}", v), + }) +} + +/// Compute the negation of a value. +pub fn neg(value: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match value { + Int(v) => Int(v.checked_neg().ok_or_else(too_large)?), + Float(v) => Float(-v), + Decimal(v) => Decimal(-v), + Length(v) => Length(-v), + Angle(v) => Angle(-v), + Ratio(v) => Ratio(-v), + Relative(v) => Relative(-v), + Fraction(v) => Fraction(-v), + Duration(v) => Duration(-v), + Datetime(_) => mismatch!("cannot apply unary '-' to {}", value), + v => mismatch!("cannot apply '-' to {}", v), + }) +} + +/// Compute the sum of two values. +pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + + (Int(a), Int(b)) => Int(a.checked_add(b).ok_or_else(too_large)?), + (Int(a), Float(b)) => Float(a as f64 + b), + (Float(a), Int(b)) => Float(a + b as f64), + (Float(a), Float(b)) => Float(a + b), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_add(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_add(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_add(b) + .ok_or_else(too_large)?, + ), + + (Angle(a), Angle(b)) => Angle(a + b), + + (Length(a), Length(b)) => Length(a + b), + (Length(a), Ratio(b)) => Relative(b + a), + (Length(a), Relative(b)) => Relative(b + a), + + (Ratio(a), Length(b)) => Relative(a + b), + (Ratio(a), Ratio(b)) => Ratio(a + b), + (Ratio(a), Relative(b)) => Relative(b + a), + + (Relative(a), Length(b)) => Relative(a + b), + (Relative(a), Ratio(b)) => Relative(a + b), + (Relative(a), Relative(b)) => Relative(a + b), + + (Fraction(a), Fraction(b)) => Fraction(a + b), + + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Bytes(a), Bytes(b)) => Bytes(a + b), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { + Stroke::from_pair(color, thickness).into_value() + } + (Gradient(gradient), Length(thickness)) + | (Length(thickness), Gradient(gradient)) => { + Stroke::from_pair(gradient, thickness).into_value() + } + (Pattern(pattern), Length(thickness)) | (Length(thickness), Pattern(pattern)) => { + Stroke::from_pair(pattern, thickness).into_value() + } + + (Duration(a), Duration(b)) => Duration(a + b), + (Datetime(a), Duration(b)) => Datetime(a + b), + (Duration(a), Datetime(b)) => Datetime(b + a), + + // Type compatibility. + (Type(a), Str(b)) => Str(format_str!("{a}{b}")), + (Str(a), Type(b)) => Str(format_str!("{a}{b}")), + + (Dyn(a), Dyn(b)) => { + // Alignments can be summed. + if let (Some(&a), Some(&b)) = + (a.downcast::<Alignment>(), b.downcast::<Alignment>()) + { + return Ok((a + b)?.into_value()); + } + + mismatch!("cannot add {} and {}", a, b); + } + + (a, b) => mismatch!("cannot add {} and {}", a, b), + }) +} + +/// Compute the difference of two values. +pub fn sub(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(too_large)?), + (Int(a), Float(b)) => Float(a as f64 - b), + (Float(a), Int(b)) => Float(a - b as f64), + (Float(a), Float(b)) => Float(a - b), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_sub(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_sub(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_sub(b) + .ok_or_else(too_large)?, + ), + + (Angle(a), Angle(b)) => Angle(a - b), + + (Length(a), Length(b)) => Length(a - b), + (Length(a), Ratio(b)) => Relative(-b + a), + (Length(a), Relative(b)) => Relative(-b + a), + + (Ratio(a), Length(b)) => Relative(a + -b), + (Ratio(a), Ratio(b)) => Ratio(a - b), + (Ratio(a), Relative(b)) => Relative(-b + a), + + (Relative(a), Length(b)) => Relative(a + -b), + (Relative(a), Ratio(b)) => Relative(a + -b), + (Relative(a), Relative(b)) => Relative(a - b), + + (Fraction(a), Fraction(b)) => Fraction(a - b), + + (Duration(a), Duration(b)) => Duration(a - b), + (Datetime(a), Duration(b)) => Datetime(a - b), + (Datetime(a), Datetime(b)) => Duration((a - b)?), + + (a, b) => mismatch!("cannot subtract {1} from {0}", a, b), + }) +} + +/// Compute the product of two values. +pub fn mul(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(too_large)?), + (Int(a), Float(b)) => Float(a as f64 * b), + (Float(a), Int(b)) => Float(a * b as f64), + (Float(a), Float(b)) => Float(a * b), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_mul(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_mul(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_mul(b) + .ok_or_else(too_large)?, + ), + + (Length(a), Int(b)) => Length(a * b as f64), + (Length(a), Float(b)) => Length(a * b), + (Length(a), Ratio(b)) => Length(a * b.get()), + (Int(a), Length(b)) => Length(b * a as f64), + (Float(a), Length(b)) => Length(b * a), + (Ratio(a), Length(b)) => Length(b * a.get()), + + (Angle(a), Int(b)) => Angle(a * b as f64), + (Angle(a), Float(b)) => Angle(a * b), + (Angle(a), Ratio(b)) => Angle(a * b.get()), + (Int(a), Angle(b)) => Angle(a as f64 * b), + (Float(a), Angle(b)) => Angle(a * b), + (Ratio(a), Angle(b)) => Angle(a.get() * b), + + (Ratio(a), Ratio(b)) => Ratio(a * b), + (Ratio(a), Int(b)) => Ratio(a * b as f64), + (Ratio(a), Float(b)) => Ratio(a * b), + (Int(a), Ratio(b)) => Ratio(a as f64 * b), + (Float(a), Ratio(b)) => Ratio(a * b), + + (Relative(a), Int(b)) => Relative(a * b as f64), + (Relative(a), Float(b)) => Relative(a * b), + (Relative(a), Ratio(b)) => Relative(a * b.get()), + (Int(a), Relative(b)) => Relative(a as f64 * b), + (Float(a), Relative(b)) => Relative(a * b), + (Ratio(a), Relative(b)) => Relative(a.get() * b), + + (Fraction(a), Int(b)) => Fraction(a * b as f64), + (Fraction(a), Float(b)) => Fraction(a * b), + (Fraction(a), Ratio(b)) => Fraction(a * b.get()), + (Int(a), Fraction(b)) => Fraction(a as f64 * b), + (Float(a), Fraction(b)) => Fraction(a * b), + (Ratio(a), Fraction(b)) => Fraction(a.get() * b), + + (Str(a), Int(b)) => Str(a.repeat(Value::Int(b).cast()?)?), + (Int(a), Str(b)) => Str(b.repeat(Value::Int(a).cast()?)?), + (Array(a), Int(b)) => Array(a.repeat(Value::Int(b).cast()?)?), + (Int(a), Array(b)) => Array(b.repeat(Value::Int(a).cast()?)?), + (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), + (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), + + (Int(a), Duration(b)) => Duration(b * (a as f64)), + (Float(a), Duration(b)) => Duration(b * a), + (Duration(a), Int(b)) => Duration(a * (b as f64)), + (Duration(a), Float(b)) => Duration(a * b), + + (a, b) => mismatch!("cannot multiply {} with {}", a, b), + }) +} + +/// Compute the quotient of two values. +pub fn div(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use Value::*; + if is_zero(&rhs) { + bail!("cannot divide by zero"); + } + + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Float(a as f64 / b as f64), + (Int(a), Float(b)) => Float(a as f64 / b), + (Float(a), Int(b)) => Float(a / b as f64), + (Float(a), Float(b)) => Float(a / b), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_div(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_div(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_div(b) + .ok_or_else(too_large)?, + ), + + (Length(a), Int(b)) => Length(a / b as f64), + (Length(a), Float(b)) => Length(a / b), + (Length(a), Length(b)) => Float(try_div_length(a, b)?), + (Length(a), Relative(b)) if b.rel.is_zero() => Float(try_div_length(a, b.abs)?), + + (Angle(a), Int(b)) => Angle(a / b as f64), + (Angle(a), Float(b)) => Angle(a / b), + (Angle(a), Angle(b)) => Float(a / b), + + (Ratio(a), Int(b)) => Ratio(a / b as f64), + (Ratio(a), Float(b)) => Ratio(a / b), + (Ratio(a), Ratio(b)) => Float(a / b), + (Ratio(a), Relative(b)) if b.abs.is_zero() => Float(a / b.rel), + + (Relative(a), Int(b)) => Relative(a / b as f64), + (Relative(a), Float(b)) => Relative(a / b), + (Relative(a), Length(b)) if a.rel.is_zero() => Float(try_div_length(a.abs, b)?), + (Relative(a), Ratio(b)) if a.abs.is_zero() => Float(a.rel / b), + (Relative(a), Relative(b)) => Float(try_div_relative(a, b)?), + + (Fraction(a), Int(b)) => Fraction(a / b as f64), + (Fraction(a), Float(b)) => Fraction(a / b), + (Fraction(a), Fraction(b)) => Float(a / b), + + (Duration(a), Int(b)) => Duration(a / (b as f64)), + (Duration(a), Float(b)) => Duration(a / b), + (Duration(a), Duration(b)) => Float(a / b), + + (a, b) => mismatch!("cannot divide {} by {}", a, b), + }) +} + +/// Whether a value is a numeric zero. +fn is_zero(v: &Value) -> bool { + use Value::*; + match *v { + Int(v) => v == 0, + Float(v) => v == 0.0, + Decimal(v) => v.is_zero(), + Length(v) => v.is_zero(), + Angle(v) => v.is_zero(), + Ratio(v) => v.is_zero(), + Relative(v) => v.is_zero(), + Fraction(v) => v.is_zero(), + Duration(v) => v.is_zero(), + _ => false, + } +} + +/// Try to divide two lengths. +fn try_div_length(a: Length, b: Length) -> StrResult<f64> { + a.try_div(b).ok_or_else(|| "cannot divide these two lengths".into()) +} + +/// Try to divide two relative lengths. +fn try_div_relative(a: Rel<Length>, b: Rel<Length>) -> StrResult<f64> { + a.try_div(b) + .ok_or_else(|| "cannot divide these two relative lengths".into()) +} + +/// Compute the logical "not" of a value. +pub fn not(value: Value) -> HintedStrResult<Value> { + match value { + Value::Bool(b) => Ok(Value::Bool(!b)), + v => mismatch!("cannot apply 'not' to {}", v), + } +} + +/// Compute the logical "and" of two values. +pub fn and(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + match (lhs, rhs) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b)), + (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), + } +} + +/// Compute the logical "or" of two values. +pub fn or(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + match (lhs, rhs) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b)), + (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b), + } +} + +/// Compute whether two values are equal. +pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + Ok(Value::Bool(equal(&lhs, &rhs))) +} + +/// Compute whether two values are unequal. +pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + Ok(Value::Bool(!equal(&lhs, &rhs))) +} + +macro_rules! comparison { + ($name:ident, $op:tt, $($pat:tt)*) => { + /// Compute how a value compares with another value. + pub fn $name(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + let ordering = compare(&lhs, &rhs)?; + Ok(Value::Bool(matches!(ordering, $($pat)*))) + } + }; +} + +comparison!(lt, "<", Ordering::Less); +comparison!(leq, "<=", Ordering::Less | Ordering::Equal); +comparison!(gt, ">", Ordering::Greater); +comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); + +/// Determine whether two values are equal. +pub fn equal(lhs: &Value, rhs: &Value) -> bool { + use Value::*; + match (lhs, rhs) { + // Compare reflexively. + (None, None) => true, + (Auto, Auto) => true, + (Bool(a), Bool(b)) => a == b, + (Int(a), Int(b)) => a == b, + (Float(a), Float(b)) => a == b, + (Decimal(a), Decimal(b)) => a == b, + (Length(a), Length(b)) => a == b, + (Angle(a), Angle(b)) => a == b, + (Ratio(a), Ratio(b)) => a == b, + (Relative(a), Relative(b)) => a == b, + (Fraction(a), Fraction(b)) => a == b, + (Color(a), Color(b)) => a == b, + (Symbol(a), Symbol(b)) => a == b, + (Version(a), Version(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Bytes(a), Bytes(b)) => a == b, + (Label(a), Label(b)) => a == b, + (Content(a), Content(b)) => a == b, + (Array(a), Array(b)) => a == b, + (Dict(a), Dict(b)) => a == b, + (Func(a), Func(b)) => a == b, + (Args(a), Args(b)) => a == b, + (Type(a), Type(b)) => a == b, + (Module(a), Module(b)) => a == b, + (Plugin(a), Plugin(b)) => a == b, + (Datetime(a), Datetime(b)) => a == b, + (Duration(a), Duration(b)) => a == b, + (Dyn(a), Dyn(b)) => a == b, + + // Some technically different things should compare equal. + (&Int(i), &Float(f)) | (&Float(f), &Int(i)) => i as f64 == f, + (&Int(i), &Decimal(d)) | (&Decimal(d), &Int(i)) => { + crate::foundations::Decimal::from(i) == d + } + (&Length(len), &Relative(rel)) | (&Relative(rel), &Length(len)) => { + len == rel.abs && rel.rel.is_zero() + } + (&Ratio(rat), &Relative(rel)) | (&Relative(rel), &Ratio(rat)) => { + rat == rel.rel && rel.abs.is_zero() + } + + // Type compatibility. + (Type(ty), Str(str)) | (Str(str), Type(ty)) => ty.compat_name() == str.as_str(), + + _ => false, + } +} + +/// Compare two values. +pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> { + use Value::*; + Ok(match (lhs, rhs) { + (Bool(a), Bool(b)) => a.cmp(b), + (Int(a), Int(b)) => a.cmp(b), + (Float(a), Float(b)) => try_cmp_values(a, b)?, + (Decimal(a), Decimal(b)) => a.cmp(b), + (Length(a), Length(b)) => try_cmp_values(a, b)?, + (Angle(a), Angle(b)) => a.cmp(b), + (Ratio(a), Ratio(b)) => a.cmp(b), + (Relative(a), Relative(b)) => try_cmp_values(a, b)?, + (Fraction(a), Fraction(b)) => a.cmp(b), + (Version(a), Version(b)) => a.cmp(b), + (Str(a), Str(b)) => a.cmp(b), + + // Some technically different things should be comparable. + (Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?, + (Float(a), Int(b)) => try_cmp_values(a, &(*b as f64))?, + (Int(a), Decimal(b)) => crate::foundations::Decimal::from(*a).cmp(b), + (Decimal(a), Int(b)) => a.cmp(&crate::foundations::Decimal::from(*b)), + (Length(a), Relative(b)) if b.rel.is_zero() => try_cmp_values(a, &b.abs)?, + (Ratio(a), Relative(b)) if b.abs.is_zero() => a.cmp(&b.rel), + (Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?, + (Relative(a), Ratio(b)) if a.abs.is_zero() => a.rel.cmp(b), + + (Duration(a), Duration(b)) => a.cmp(b), + (Datetime(a), Datetime(b)) => try_cmp_datetimes(a, b)?, + (Array(a), Array(b)) => try_cmp_arrays(a.as_slice(), b.as_slice())?, + + _ => mismatch!("cannot compare {} and {}", lhs, rhs), + }) +} + +/// Try to compare two values. +fn try_cmp_values<T: PartialOrd + Repr>(a: &T, b: &T) -> StrResult<Ordering> { + a.partial_cmp(b) + .ok_or_else(|| eco_format!("cannot compare {} with {}", a.repr(), b.repr())) +} + +/// Try to compare two datetimes. +fn try_cmp_datetimes(a: &Datetime, b: &Datetime) -> StrResult<Ordering> { + a.partial_cmp(b) + .ok_or_else(|| eco_format!("cannot compare {} and {}", a.kind(), b.kind())) +} + +/// Try to compare arrays of values lexicographically. +fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> { + a.iter() + .zip(b.iter()) + .find_map(|(first, second)| { + match compare(first, second) { + // Keep searching for a pair of elements that isn't equal. + Ok(Ordering::Equal) => None, + // Found a pair which either is not equal or not comparable, so + // we stop searching. + result => Some(result), + } + }) + .unwrap_or_else(|| { + // The two arrays are equal up to the shortest array's extent, + // so compare their lengths instead. + Ok(a.len().cmp(&b.len())) + }) +} + +/// Test whether one value is "in" another one. +pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Value::Bool(b)) + } else { + mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) + } +} + +/// Test whether one value is "not in" another one. +pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Value::Bool(!b)) + } else { + mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) + } +} + +/// Test for containment. +pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { + use Value::*; + match (lhs, rhs) { + (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), + (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), + (Str(a), Dict(b)) => Some(b.contains(a)), + (a, Array(b)) => Some(b.contains(a.clone())), + + // Type compatibility. + (Type(a), Str(b)) => Some(b.as_str().contains(a.compat_name())), + (Type(a), Dict(b)) => Some(b.contains(a.compat_name())), + + _ => Option::None, + } +} + +#[cold] +fn too_large() -> &'static str { + "value is too large" +} diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs new file mode 100644 index 00000000..31107dc3 --- /dev/null +++ b/crates/typst-library/src/foundations/plugin.rs @@ -0,0 +1,365 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex}; + +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; +use wasmi::{AsContext, AsContextMut}; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{func, repr, scope, ty, Bytes}; +use crate::World; + +/// A WebAssembly plugin. +/// +/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin +/// functions may accept multiple [byte buffers]($bytes) as arguments and return +/// a single byte buffer. They should typically be wrapped in idiomatic Typst +/// functions that perform the necessary conversions between native Typst types +/// and bytes. +/// +/// Plugins run in isolation from your system, which means that printing, +/// reading files, or anything like that will not be supported for security +/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit +/// shared WebAssembly library. Many compilers will use the +/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. +/// emscripten), which allows printing, reading files, etc. This ABI will not +/// directly work with Typst. You will either need to compile to a different +/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/blob/master/wasi-stub). +/// +/// # Plugins and Packages +/// Plugins are distributed as packages. A package can make use of a plugin +/// simply by including a WebAssembly file and loading it. Because the +/// byte-based plugin interface is quite low-level, plugins are typically +/// exposed through wrapper functions, that also live in the same package. +/// +/// # Purity +/// Plugin functions must be pure: Given the same arguments, they must always +/// return the same value. The reason for this is that Typst functions must be +/// pure (which is quite fundamental to the language design) and, since Typst +/// function can call plugin functions, this requirement is inherited. In +/// particular, if a plugin function is called twice with the same arguments, +/// Typst might cache the results and call your function only once. +/// +/// # Example +/// ```example +/// #let myplugin = plugin("hello.wasm") +/// #let concat(a, b) = str( +/// myplugin.concatenate( +/// bytes(a), +/// bytes(b), +/// ) +/// ) +/// +/// #concat("hello", "world") +/// ``` +/// +/// # Protocol +/// To be used as a plugin, a WebAssembly module must conform to the following +/// protocol: +/// +/// ## Exports +/// A plugin module can export functions to make them callable from Typst. To +/// conform to the protocol, an exported function should: +/// +/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as +/// lengths, so `usize/size_t` may be preferable), and return one 32-bit +/// integer. +/// +/// - The function should first allocate a buffer `buf` of length +/// `a_1 + a_2 + ... + a_n`, and then call +/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. +/// +/// - The `a_1` first bytes of the buffer now constitute the first argument, the +/// `a_2` next bytes the second argument, and so on. +/// +/// - The function can now do its job with the arguments and produce an output +/// buffer. Before returning, it should call +/// `wasm_minimal_protocol_send_result_to_host` to send its result back to the +/// host. +/// +/// - To signal success, the function should return `0`. +/// +/// - To signal an error, the function should return `1`. The written buffer is +/// then interpreted as an UTF-8 encoded error message. +/// +/// ## Imports +/// Plugin modules need to import two functions that are provided by the runtime. +/// (Types and functions are described using WAT syntax.) +/// +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// +/// Writes the arguments for the current function into a plugin-allocated +/// buffer. When a plugin function is called, it +/// [receives the lengths](#exports) of its input buffers as arguments. It +/// should then allocate a buffer whose capacity is at least the sum of these +/// lengths. It should then call this function with a `ptr` to the buffer to +/// fill it with the arguments, one after another. +/// +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// +/// Sends the output of the current function to the host (Typst). The first +/// parameter shall be a pointer to a buffer (`ptr`), while the second is the +/// length of that buffer (`len`). The memory pointed at by `ptr` can be freed +/// immediately after this function returns. If the message should be +/// interpreted as an error message, it should be encoded as UTF-8. +/// +/// # Resources +/// For more resources, check out the +/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). +/// It contains: +/// +/// - A list of example plugin implementations and a test runner for these +/// examples +/// - Wrappers to help you write your plugin in Rust (Zig wrapper in +/// development) +/// - A stubber for WASI +#[ty(scope, cast)] +#[derive(Clone)] +pub struct Plugin(Arc<Repr>); + +/// The internal representation of a plugin. +struct Repr { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The function defined by the WebAssembly module. + functions: Vec<(EcoString, wasmi::Func)>, + /// Owns all data associated with the WebAssembly module. + store: Mutex<Store>, +} + +/// Owns all data associated with the WebAssembly module. +type Store = wasmi::Store<StoreData>; + +/// If there was an error reading/writing memory, keep the offset + length to +/// display an error message. +struct MemoryError { + offset: u32, + length: u32, + write: bool, +} +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct StoreData { + args: Vec<Bytes>, + output: Vec<u8>, + memory_error: Option<MemoryError>, +} + +#[scope] +impl Plugin { + /// Creates a new plugin from a WebAssembly file. + #[func(constructor)] + pub fn construct( + /// The engine. + engine: &mut Engine, + /// Path to a WebAssembly file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, + ) -> SourceResult<Plugin> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + Plugin::new(data).at(span) + } +} + +impl Plugin { + /// Create a new plugin from raw WebAssembly bytes. + #[comemo::memoize] + #[typst_macros::time(name = "load plugin")] + pub fn new(bytes: Bytes) -> StrResult<Plugin> { + let engine = wasmi::Engine::default(); + let module = wasmi::Module::new(&engine, bytes.as_slice()) + .map_err(|err| format!("failed to load WebAssembly module ({err})"))?; + + let mut linker = wasmi::Linker::new(&engine); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_send_result_to_host", + wasm_minimal_protocol_send_result_to_host, + ) + .unwrap(); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_write_args_to_buffer", + wasm_minimal_protocol_write_args_to_buffer, + ) + .unwrap(); + + let mut store = Store::new(&engine, StoreData::default()); + let instance = linker + .instantiate(&mut store, &module) + .and_then(|pre_instance| pre_instance.start(&mut store)) + .map_err(|e| eco_format!("{e}"))?; + + // Ensure that the plugin exports its memory. + if !matches!( + instance.get_export(&store, "memory"), + Some(wasmi::Extern::Memory(_)) + ) { + bail!("plugin does not export its memory"); + } + + // Collect exported functions. + let functions = instance + .exports(&store) + .filter_map(|export| { + let name = export.name().into(); + export.into_func().map(|func| (name, func)) + }) + .collect(); + + Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + } + + /// Call the plugin function with the given `name`. + #[comemo::memoize] + #[typst_macros::time(name = "call plugin")] + pub fn call(&self, name: &str, args: Vec<Bytes>) -> StrResult<Bytes> { + // Find the function with the given name. + let func = self + .0 + .functions + .iter() + .find(|(v, _)| v == name) + .map(|&(_, func)| func) + .ok_or_else(|| { + eco_format!("plugin does not contain a function called {name}") + })?; + + let mut store = self.0.store.lock().unwrap(); + let ty = func.ty(store.as_context()); + + // Check function signature. + if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { + bail!( + "plugin function `{name}` has a parameter that is not a 32-bit integer" + ); + } + if ty.results() != [wasmi::core::ValType::I32] { + bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + } + + // Check inputs. + let expected = ty.params().len(); + let given = args.len(); + if expected != given { + bail!( + "plugin function takes {expected} argument{}, but {given} {} given", + if expected == 1 { "" } else { "s" }, + if given == 1 { "was" } else { "were" }, + ); + } + + // Collect the lengths of the argument buffers. + let lengths = args + .iter() + .map(|a| wasmi::Val::I32(a.len() as i32)) + .collect::<Vec<_>>(); + + // Store the input data. + store.data_mut().args = args; + + // Call the function. + let mut code = wasmi::Val::I32(-1); + func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + .map_err(|err| eco_format!("plugin panicked: {err}"))?; + if let Some(MemoryError { offset, length, write }) = + store.data_mut().memory_error.take() + { + return Err(eco_format!( + "plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", + kind = if write { "write" } else { "read" } + )); + } + + // Extract the returned data. + let output = std::mem::take(&mut store.data_mut().output); + + // Parse the functions return value. + match code { + wasmi::Val::I32(0) => {} + wasmi::Val::I32(1) => match std::str::from_utf8(&output) { + Ok(message) => bail!("plugin errored with: {message}"), + Err(_) => { + bail!("plugin errored, but did not return a valid error message") + } + }, + _ => bail!("plugin did not respect the protocol"), + }; + + Ok(output.into()) + } + + /// An iterator over all the function names defined by the plugin. + pub fn iter(&self) -> impl Iterator<Item = &EcoString> { + self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Plugin(..)") + } +} + +impl repr::Repr for Plugin { + fn repr(&self) -> EcoString { + "plugin(..)".into() + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.0.bytes == other.0.bytes + } +} + +impl Hash for Plugin { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.bytes.hash(state); + } +} + +/// Write the arguments to the plugin function into the plugin's memory. +fn wasm_minimal_protocol_write_args_to_buffer( + mut caller: wasmi::Caller<StoreData>, + ptr: u32, +) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let arguments = std::mem::take(&mut caller.data_mut().args); + let mut offset = ptr as usize; + for arg in arguments { + if memory.write(&mut caller, offset, arg.as_slice()).is_err() { + caller.data_mut().memory_error = Some(MemoryError { + offset: offset as u32, + length: arg.len() as u32, + write: true, + }); + return; + } + offset += arg.len(); + } +} + +/// Extracts the output of the plugin function from the plugin's memory. +fn wasm_minimal_protocol_send_result_to_host( + mut caller: wasmi::Caller<StoreData>, + ptr: u32, + len: u32, +) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let mut buffer = std::mem::take(&mut caller.data_mut().output); + buffer.resize(len as usize, 0); + if memory.read(&caller, ptr as _, &mut buffer).is_err() { + caller.data_mut().memory_error = + Some(MemoryError { offset: ptr, length: len, write: false }); + return; + } + caller.data_mut().output = buffer; +} diff --git a/crates/typst-library/src/foundations/repr.rs b/crates/typst-library/src/foundations/repr.rs new file mode 100644 index 00000000..e219b0f1 --- /dev/null +++ b/crates/typst-library/src/foundations/repr.rs @@ -0,0 +1,235 @@ +//! Debug representation of values. + +use ecow::{eco_format, EcoString}; +use typst_utils::round_with_precision; + +use crate::foundations::{func, Str, Value}; + +/// The Unicode minus sign. +pub const MINUS_SIGN: &str = "\u{2212}"; + +/// Returns the string representation of a value. +/// +/// When inserted into content, most values are displayed as this representation +/// in monospace with syntax-highlighting. The exceptions are `{none}`, +/// integers, floats, strings, content, and functions. +/// +/// **Note:** This function is for debugging purposes. Its output should not be +/// considered stable and may change at any time! +/// +/// # Example +/// ```example +/// #none vs #repr(none) \ +/// #"hello" vs #repr("hello") \ +/// #(1, 2) vs #repr((1, 2)) \ +/// #[*Hi*] vs #repr([*Hi*]) +/// ``` +#[func(title = "Representation")] +pub fn repr( + /// The value whose string representation to produce. + value: Value, +) -> Str { + value.repr().into() +} + +/// A trait that defines the `repr` of a Typst value. +pub trait Repr { + /// Return the debug representation of the value. + fn repr(&self) -> EcoString; +} + +/// Format an integer in a base. +pub fn format_int_with_base(mut n: i64, base: i64) -> EcoString { + if n == 0 { + return "0".into(); + } + + // The largest output is `to_base(i64::MIN, 2)`, which is 64 bytes long, + // plus the length of the minus sign. + const SIZE: usize = 64 + MINUS_SIGN.len(); + let mut digits = [b'\0'; SIZE]; + let mut i = SIZE; + + // It's tempting to take the absolute value, but this will fail for i64::MIN. + // Instead, we turn n negative, as -i64::MAX is perfectly representable. + let negative = n < 0; + if n > 0 { + n = -n; + } + + while n != 0 { + let digit = char::from_digit(-(n % base) as u32, base as u32); + i -= 1; + digits[i] = digit.unwrap_or('?') as u8; + n /= base; + } + + if negative { + let prev = i; + i -= MINUS_SIGN.len(); + digits[i..prev].copy_from_slice(MINUS_SIGN.as_bytes()); + } + + std::str::from_utf8(&digits[i..]).unwrap_or_default().into() +} + +/// Converts a float to a string representation with a specific precision and a +/// unit, all with a single allocation. +/// +/// The returned string is always valid Typst code. As such, it might not be a +/// float literal. For example, it may return `"float.inf"`. +pub fn format_float( + mut value: f64, + precision: Option<u8>, + force_separator: bool, + unit: &str, +) -> EcoString { + if let Some(p) = precision { + value = round_with_precision(value, p as i16); + } + // Debug for f64 always prints a decimal separator, while Display only does + // when necessary. + let unit_multiplication = if unit.is_empty() { "" } else { " * 1" }; + if value.is_nan() { + eco_format!("float.nan{unit_multiplication}{unit}") + } else if value.is_infinite() { + let sign = if value < 0.0 { "-" } else { "" }; + eco_format!("{sign}float.inf{unit_multiplication}{unit}") + } else if force_separator { + eco_format!("{value:?}{unit}") + } else { + eco_format!("{value}{unit}") + } +} + +/// Converts a float to a string representation with a precision of three +/// decimal places. This is intended to be used as part of a larger structure +/// containing multiple float components, such as colors. +pub fn format_float_component(value: f64) -> EcoString { + format_float(value, Some(3), false, "") +} + +/// Converts a float to a string representation with a precision of two decimal +/// places, followed by a unit. +pub fn format_float_with_unit(value: f64, unit: &str) -> EcoString { + format_float(value, Some(2), false, unit) +} + +/// Converts a float to a string that can be used to display the float as text. +pub fn display_float(value: f64) -> EcoString { + if value.is_nan() { + "NaN".into() + } else if value.is_infinite() { + let sign = if value < 0.0 { MINUS_SIGN } else { "" }; + eco_format!("{sign}∞") + } else if value < 0.0 { + eco_format!("{}{}", MINUS_SIGN, value.abs()) + } else { + eco_format!("{}", value.abs()) + } +} + +/// Formats pieces separated with commas and a final "and" or "or". +pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String { + let mut buf = String::new(); + for (i, part) in pieces.iter().enumerate() { + match i { + 0 => {} + 1 if pieces.len() == 2 => { + buf.push(' '); + buf.push_str(last); + buf.push(' '); + } + i if i + 1 == pieces.len() => { + buf.push_str(", "); + buf.push_str(last); + buf.push(' '); + } + _ => buf.push_str(", "), + } + buf.push_str(part.as_ref()); + } + buf +} + +/// Formats a comma-separated list. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String { + const MAX_WIDTH: usize = 50; + + let mut buf = String::new(); + let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>() + + 2 * pieces.len().saturating_sub(1); + + if len <= MAX_WIDTH { + for (i, piece) in pieces.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(piece.as_ref()); + } + if trailing_comma { + buf.push(','); + } + } else { + for piece in pieces { + buf.push_str(piece.as_ref().trim()); + buf.push_str(",\n"); + } + } + + buf +} + +/// Formats an array-like construct. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String { + let list = pretty_comma_list(parts, trailing_comma); + let mut buf = String::new(); + buf.push('('); + if list.contains('\n') { + buf.push('\n'); + for (i, line) in list.lines().enumerate() { + if i > 0 { + buf.push('\n'); + } + buf.push_str(" "); + buf.push_str(line); + } + buf.push('\n'); + } else { + buf.push_str(&list); + } + buf.push(')'); + buf +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_base() { + assert_eq!(&format_int_with_base(0, 10), "0"); + assert_eq!(&format_int_with_base(0, 16), "0"); + assert_eq!(&format_int_with_base(0, 36), "0"); + assert_eq!( + &format_int_with_base(i64::MAX, 2), + "111111111111111111111111111111111111111111111111111111111111111" + ); + assert_eq!( + &format_int_with_base(i64::MIN, 2), + "\u{2212}1000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!(&format_int_with_base(i64::MAX, 10), "9223372036854775807"); + assert_eq!(&format_int_with_base(i64::MIN, 10), "\u{2212}9223372036854775808"); + assert_eq!(&format_int_with_base(i64::MAX, 16), "7fffffffffffffff"); + assert_eq!(&format_int_with_base(i64::MIN, 16), "\u{2212}8000000000000000"); + assert_eq!(&format_int_with_base(i64::MAX, 36), "1y2p0ij32e8e7"); + assert_eq!(&format_int_with_base(i64::MIN, 36), "\u{2212}1y2p0ij32e8e8"); + } +} diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs new file mode 100644 index 00000000..b51f8caa --- /dev/null +++ b/crates/typst-library/src/foundations/scope.rs @@ -0,0 +1,416 @@ +#[doc(inline)] +pub use typst_macros::category; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; + +use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::Span; +use typst_utils::Static; + +use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::foundations::{ + Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, + NativeType, Type, Value, +}; +use crate::Library; + +/// A stack of scopes. +#[derive(Debug, Default, Clone)] +pub struct Scopes<'a> { + /// The active scope. + pub top: Scope, + /// The stack of lower scopes. + pub scopes: Vec<Scope>, + /// The standard library. + pub base: Option<&'a Library>, +} + +impl<'a> Scopes<'a> { + /// Create a new, empty hierarchy of scopes. + pub fn new(base: Option<&'a Library>) -> Self { + Self { top: Scope::new(), scopes: vec![], base } + } + + /// Enter a new scope. + pub fn enter(&mut self) { + self.scopes.push(std::mem::take(&mut self.top)); + } + + /// Exit the topmost scope. + /// + /// This panics if no scope was entered. + pub fn exit(&mut self) { + self.top = self.scopes.pop().expect("no pushed scope"); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.global.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| unknown_variable(var)) + } + + /// Try to access a variable immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.math.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| { + unknown_variable_math( + var, + self.base.is_some_and(|base| base.global.scope().get(var).is_some()), + ) + }) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + })? + } + + /// Check if an std variable is shadowed. + pub fn check_std_shadowed(&self, var: &str) -> bool { + self.base.is_some_and(|base| base.global.scope().get(var).is_some()) + && std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .any(|scope| scope.get(var).is_some()) + } +} + +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() +} + +/// The error message when a variable is not found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); + } + + res +} + +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", + var.chars() + .flat_map(|c| [' ', c]) + .skip(1) + .collect::<EcoString>() + )); + res.hint(eco_format!( + "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" + )); + } + + res +} + +/// A map from binding names to values. +#[derive(Default, Clone)] +pub struct Scope { + map: IndexMap<EcoString, Slot>, + deduplicate: bool, + category: Option<Category>, +} + +impl Scope { + /// Create a new empty scope. + pub fn new() -> Self { + Default::default() + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self { deduplicate: true, ..Default::default() } + } + + /// Enter a new category. + pub fn category(&mut self, category: Category) { + self.category = Some(category); + } + + /// Reset the category. + pub fn reset_category(&mut self) { + self.category = None; + } + + /// Bind a value to a name. + #[track_caller] + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { + self.define_spanned(name, value, Span::detached()) + } + + /// Bind a value to a name defined by an identifier. + #[track_caller] + pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { + self.define_spanned(ident.get().clone(), value, ident.span()) + } + + /// Bind a value to a name. + #[track_caller] + pub fn define_spanned( + &mut self, + name: impl Into<EcoString>, + value: impl IntoValue, + span: Span, + ) { + let name = name.into(); + + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Normal, self.category), + ); + } + + /// Define a captured, immutable binding. + pub fn define_captured( + &mut self, + name: EcoString, + value: Value, + capturer: Capturer, + span: Span, + ) { + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), + ); + } + + /// Define a native function through a Rust type that shadows the function. + pub fn define_func<T: NativeFunc>(&mut self) { + let data = T::data(); + self.define(data.name, Func::from(data)); + } + + /// Define a native function with raw function data. + pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { + self.define(data.name, Func::from(data)); + } + + /// Define a native type. + pub fn define_type<T: NativeType>(&mut self) { + let data = T::data(); + self.define(data.name, Type::from(data)); + } + + /// Define a native element. + pub fn define_elem<T: NativeElement>(&mut self) { + let data = T::data(); + self.define(data.name, Element::from(data)); + } + + /// Define a module. + pub fn define_module(&mut self, module: Module) { + self.define(module.name().clone(), module); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.map.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option<HintedStrResult<&mut Value>> { + self.map + .get_mut(var) + .map(Slot::write) + .map(|res| res.map_err(HintedString::from)) + } + + /// Get the span of a definition. + pub fn get_span(&self, var: &str) -> Option<Span> { + Some(self.map.get(var)?.span) + } + + /// Get the category of a definition. + pub fn get_category(&self, var: &str) -> Option<Category> { + self.map.get(var)?.category + } + + /// Iterate over all definitions. + pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> { + self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; + f.debug_map() + .entries(self.map.iter().map(|(k, v)| (k, v.read()))) + .finish() + } +} + +impl Hash for Scope { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.map.len()); + for item in &self.map { + item.hash(state); + } + self.deduplicate.hash(state); + self.category.hash(state); + } +} + +/// Defines the associated scope of a Rust type. +pub trait NativeScope { + /// The constructor function for the type, if any. + fn constructor() -> Option<&'static NativeFuncData>; + + /// Get the associated scope for the type. + fn scope() -> Scope; +} + +/// A slot where a value is stored. +#[derive(Clone, Hash)] +struct Slot { + /// The stored value. + value: Value, + /// The kind of slot, determines how the value can be accessed. + kind: Kind, + /// A span associated with the stored value. + span: Span, + /// The category of the slot. + category: Option<Category>, +} + +/// The different kinds of slots. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Kind { + /// A normal, mutable binding. + Normal, + /// A captured copy of another variable. + Captured(Capturer), +} + +/// What the variable was captured by. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Capturer { + /// Captured by a function / closure. + Function, + /// Captured by a context expression. + Context, +} + +impl Slot { + /// Create a new slot. + fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self { + Self { value, span, kind, category } + } + + /// Read the value. + fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + Kind::Normal => Ok(&mut self.value), + Kind::Captured(capturer) => { + bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ) + } + } + } +} + +/// A group of related definitions. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Category(Static<CategoryData>); + +impl Category { + /// Create a new category from raw data. + pub const fn from_data(data: &'static CategoryData) -> Self { + Self(Static(data)) + } + + /// The category's name. + pub fn name(&self) -> &'static str { + self.0.name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the category. + pub fn docs(&self) -> &'static str { + self.0.docs + } +} + +impl Debug for Category { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Category({})", self.name()) + } +} + +/// Defines a category. +#[derive(Debug)] +pub struct CategoryData { + pub name: &'static str, + pub title: &'static str, + pub docs: &'static str, +} diff --git a/crates/typst-library/src/foundations/selector.rs b/crates/typst-library/src/foundations/selector.rs new file mode 100644 index 00000000..bf5449d9 --- /dev/null +++ b/crates/typst-library/src/foundations/selector.rs @@ -0,0 +1,449 @@ +use std::any::{Any, TypeId}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use smallvec::SmallVec; + +use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, + Func, Label, Reflect, Regex, Repr, Str, StyleChain, Symbol, Type, Value, +}; +use crate::introspection::{Introspector, Locatable, Location, Unqueriable}; + +/// A helper macro to create a field selector used in [`Selector::Elem`] +#[macro_export] +#[doc(hidden)] +macro_rules! __select_where { + ($ty:ty $(, $field:ident => $value:expr)* $(,)?) => {{ + #[allow(unused_mut)] + let mut fields = ::smallvec::SmallVec::new(); + $( + fields.push(( + <$ty as $crate::foundations::Fields>::Enum::$field as u8, + $crate::foundations::IntoValue::into_value($value), + )); + )* + $crate::foundations::Selector::Elem( + <$ty as $crate::foundations::NativeElement>::elem(), + Some(fields), + ) + }}; +} + +#[doc(inline)] +pub use crate::__select_where as select_where; + +/// A filter for selecting elements within the document. +/// +/// You can construct a selector in the following ways: +/// - you can use an element [function] +/// - you can filter for an element function with +/// [specific fields]($function.where) +/// - you can use a [string]($str) or [regular expression]($regex) +/// - you can use a [`{<label>}`]($label) +/// - you can use a [`location`] +/// - call the [`selector`] constructor to convert any of the above types into a +/// selector value and use the methods below to refine it +/// +/// Selectors are used to [apply styling rules]($styling/#show-rules) to +/// elements. You can also use selectors to [query] the document for certain +/// types of elements. +/// +/// Furthermore, you can pass a selector to several of Typst's built-in +/// functions to configure their behaviour. One such example is the [outline] +/// where it can be used to change which elements are listed within the outline. +/// +/// Multiple selectors can be combined using the methods shown below. However, +/// not all kinds of selectors are supported in all places, at the moment. +/// +/// # Example +/// ```example +/// #context query( +/// heading.where(level: 1) +/// .or(heading.where(level: 2)) +/// ) +/// +/// = This will be found +/// == So will this +/// === But this will not. +/// ``` +#[ty(scope, cast)] +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Selector { + /// Matches a specific type of element. + /// + /// If there is a dictionary, only elements with the fields from the + /// dictionary match. + Elem(Element, Option<SmallVec<[(u8, Value); 1]>>), + /// Matches the element at the specified location. + Location(Location), + /// Matches elements with a specific label. + Label(Label), + /// Matches text elements through a regular expression. + Regex(Regex), + /// Matches elements with a specific capability. + Can(TypeId), + /// Matches if any of the subselectors match. + Or(EcoVec<Self>), + /// Matches if all of the subselectors match. + And(EcoVec<Self>), + /// Matches all matches of `selector` before `end`. + Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, + /// Matches all matches of `selector` after `start`. + After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, +} + +impl Selector { + /// Define a simple text selector. + pub fn text(text: &str) -> StrResult<Self> { + if text.is_empty() { + bail!("text selector is empty"); + } + Ok(Self::Regex(Regex::new(®ex::escape(text)).unwrap())) + } + + /// Define a regex selector. + pub fn regex(regex: Regex) -> StrResult<Self> { + if regex.as_str().is_empty() { + bail!("regex selector is empty"); + } + if regex.is_match("") { + bail!("regex matches empty text"); + } + Ok(Self::Regex(regex)) + } + + /// Define a simple [`Selector::Can`] selector. + pub fn can<T: ?Sized + Any>() -> Self { + Self::Can(TypeId::of::<T>()) + } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool { + match self { + Self::Elem(element, dict) => { + target.elem() == *element + && dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| { + target.get(*id, styles).as_ref().ok() == Some(value) + }) + } + Self::Label(label) => target.label() == Some(*label), + Self::Can(cap) => target.func().can_type_id(*cap), + Self::Or(selectors) => { + selectors.iter().any(move |sel| sel.matches(target, styles)) + } + Self::And(selectors) => { + selectors.iter().all(move |sel| sel.matches(target, styles)) + } + Self::Location(location) => target.location() == Some(*location), + // Not supported here. + Self::Regex(_) | Self::Before { .. } | Self::After { .. } => false, + } + } +} + +#[scope] +impl Selector { + /// Turns a value into a selector. The following values are accepted: + /// - An element function like a `heading` or `figure`. + /// - A `{<label>}`. + /// - A more complex selector like `{heading.where(level: 1)}`. + #[func(constructor)] + pub fn construct( + /// Can be an element function like a `heading` or `figure`, a `{<label>}` + /// or a more complex selector like `{heading.where(level: 1)}`. + target: Selector, + ) -> Selector { + target + } + + /// Selects all elements that match this or any of the other selectors. + #[func] + pub fn or( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<Selector>, + ) -> Selector { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Selects all elements that match this and all of the other selectors. + #[func] + pub fn and( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<Selector>, + ) -> Selector { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Returns a modified selector that will only match elements that occur + /// before the first match of `end`. + #[func] + pub fn before( + self, + /// The original selection will end at the first match of `end`. + end: LocatableSelector, + /// Whether `end` itself should match or not. This is only relevant if + /// both selectors match the same type of element. Defaults to `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::Before { + selector: Arc::new(self), + end: Arc::new(end.0), + inclusive, + } + } + + /// Returns a modified selector that will only match elements that occur + /// after the first match of `start`. + #[func] + pub fn after( + self, + /// The original selection will start at the first match of `start`. + start: LocatableSelector, + /// Whether `start` itself should match or not. This is only relevant + /// if both selectors match the same type of element. Defaults to + /// `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::After { + selector: Arc::new(self), + start: Arc::new(start.0), + inclusive, + } + } +} + +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + +impl Repr for Selector { + fn repr(&self) -> EcoString { + match self { + Self::Elem(elem, dict) => { + if let Some(dict) = dict { + let dict = dict + .iter() + .map(|(id, value)| (elem.field_name(*id).unwrap(), value.clone())) + .map(|(name, value)| (EcoString::from(name).into(), value)) + .collect::<Dict>(); + eco_format!("{}.where{}", elem.name(), dict.repr()) + } else { + elem.name().into() + } + } + Self::Label(label) => label.repr(), + Self::Regex(regex) => regex.repr(), + Self::Can(cap) => eco_format!("{cap:?}"), + Self::Or(selectors) | Self::And(selectors) => { + let function = if matches!(self, Self::Or(_)) { "or" } else { "and" }; + let pieces: Vec<_> = selectors.iter().map(Selector::repr).collect(); + eco_format!("{}{}", function, repr::pretty_array_like(&pieces, false)) + } + Self::Location(loc) => loc.repr(), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + let method = + if matches!(self, Self::Before { .. }) { "before" } else { "after" }; + let inclusive_arg = if !*inclusive { ", inclusive: false" } else { "" }; + eco_format!( + "{}.{}({}{})", + selector.repr(), + method, + split.repr(), + inclusive_arg + ) + } + } + } +} + +cast! { + type Selector, + text: EcoString => Self::text(&text)?, + func: Func => func + .element() + .ok_or("only element functions can be used as selectors")? + .select(), + label: Label => Self::Label(label), + regex: Regex => Self::regex(regex)?, + location: Location => Self::Location(location), +} + +/// A selector that can be used with `query`. +/// +/// Hopefully, this is made obsolete by a more powerful query mechanism in the +/// future. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct LocatableSelector(pub Selector); + +impl LocatableSelector { + /// Resolve this selector into a location that is guaranteed to be unique. + pub fn resolve_unique( + &self, + introspector: Tracked<Introspector>, + context: Tracked<Context>, + ) -> HintedStrResult<Location> { + match &self.0 { + Selector::Location(loc) => Ok(*loc), + other => { + context.introspect()?; + Ok(introspector.query_unique(other).map(|c| c.location().unwrap())?) + } + } + } +} + +impl Reflect for LocatableSelector { + fn input() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Location>()), + CastInfo::Type(Type::of::<Selector>()), + ]) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + + fn castable(value: &Value) -> bool { + Label::castable(value) + || Func::castable(value) + || Location::castable(value) + || Selector::castable(value) + } +} + +cast! { + LocatableSelector, + self => self.0.into_value(), +} + +impl FromValue for LocatableSelector { + fn from_value(value: Value) -> HintedStrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() || elem.can::<dyn Unqueriable>() { + Err(eco_format!("{} is not locatable", elem.name()))? + } + } + Selector::Location(_) => {} + Selector::Label(_) => {} + Selector::Regex(_) => bail!("text is not locatable"), + Selector::Can(_) => bail!("capability is not locatable"), + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector)?; + } + } + Selector::Before { selector, end: split, .. } + | Selector::After { selector, start: split, .. } => { + for selector in [selector, split] { + validate(selector)?; + } + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} + +impl From<Location> for LocatableSelector { + fn from(loc: Location) -> Self { + Self(Selector::Location(loc)) + } +} + +/// A selector that can be used with show rules. +/// +/// Hopefully, this is made obsolete by a more powerful showing mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct ShowableSelector(pub Selector); + +impl Reflect for ShowableSelector { + fn input() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type(Type::of::<Symbol>()), + CastInfo::Type(Type::of::<Str>()), + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Regex>()), + CastInfo::Type(Type::of::<Selector>()), + ]) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + + fn castable(value: &Value) -> bool { + Symbol::castable(value) + || Str::castable(value) + || Label::castable(value) + || Func::castable(value) + || Regex::castable(value) + || Selector::castable(value) + } +} + +cast! { + ShowableSelector, + self => self.0.into_value(), +} + +impl FromValue for ShowableSelector { + fn from_value(value: Value) -> HintedStrResult<Self> { + fn validate(selector: &Selector, nested: bool) -> HintedStrResult<()> { + match selector { + Selector::Elem(_, _) => {} + Selector::Label(_) => {} + Selector::Regex(_) if !nested => {} + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector, true)?; + } + } + Selector::Regex(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. } => { + bail!("this selector cannot be used with show") + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector, false)?; + Ok(Self(selector)) + } +} diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs new file mode 100644 index 00000000..fc221c49 --- /dev/null +++ b/crates/typst-library/src/foundations/str.rs @@ -0,0 +1,969 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign, Deref, Range}; + +use comemo::Tracked; +use ecow::EcoString; +use serde::{Deserialize, Serialize}; +use typst_syntax::{Span, Spanned}; +use typst_utils::PicoStr; +use unicode_segmentation::UnicodeSegmentation; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, + IntoValue, Label, Repr, Type, Value, Version, +}; +use crate::layout::Alignment; + +/// Create a new [`Str`] from a format string. +#[macro_export] +#[doc(hidden)] +macro_rules! __format_str { + ($($tts:tt)*) => {{ + $crate::foundations::Str::from($crate::foundations::eco_format!($($tts)*)) + }}; +} + +#[doc(hidden)] +pub use ecow::eco_format; + +#[doc(inline)] +pub use crate::__format_str as format_str; + +/// A sequence of Unicode codepoints. +/// +/// You can iterate over the grapheme clusters of the string using a [for +/// loop]($scripting/#loops). Grapheme clusters are basically characters but +/// keep together things that belong together, e.g. multiple codepoints that +/// together form a flag emoji. Strings can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. +/// +/// Typst provides utility methods for string manipulation. Many of these +/// methods (e.g., `split`, `trim` and `replace`) operate on _patterns:_ A +/// pattern can be either a string or a [regular expression]($regex). This makes +/// the methods quite versatile. +/// +/// All lengths and indices are expressed in terms of UTF-8 bytes. Indices are +/// zero-based and negative indices wrap around to the end of the string. +/// +/// You can convert a value to a string with this type's constructor. +/// +/// # Example +/// ```example +/// #"hello world!" \ +/// #"\"hello\n world\"!" \ +/// #"1 2 3".split() \ +/// #"1,2;3".split(regex("[,;]")) \ +/// #(regex("\d+") in "ten euros") \ +/// #(regex("\d+") in "10 euros") +/// ``` +/// +/// # Escape sequences { #escapes } +/// Just like in markup, you can escape a few symbols in strings: +/// - `[\\]` for a backslash +/// - `[\"]` for a quote +/// - `[\n]` for a newline +/// - `[\r]` for a carriage return +/// - `[\t]` for a tab +/// - `[\u{1f600}]` for a hexadecimal Unicode escape sequence +#[ty(scope, cast, title = "String")] +#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct Str(EcoString); + +impl Str { + /// Create a new, empty string. + pub fn new() -> Self { + Self(EcoString::new()) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Repeat the string a number of times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + if self.0.len().checked_mul(n).is_none() { + return Err(eco_format!("cannot repeat this string {n} times")); + } + Ok(Self(self.0.repeat(n))) + } + + /// A string slice containing the entire string. + pub fn as_str(&self) -> &str { + self + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult<usize> { + self.locate_opt(index)? + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds and on a valid char boundary. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + let resolved = wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()); + + if resolved.is_some_and(|i| !self.0.is_char_boundary(i)) { + return Err(not_a_char_boundary(index)); + } + + Ok(resolved) + } +} + +#[scope] +impl Str { + /// Converts a value to a string. + /// + /// - Integers are formatted in base 10. This can be overridden with the + /// optional `base` parameter. + /// - Floats are formatted in base 10 and never in exponential notation. + /// - From labels the name is extracted. + /// - Bytes are decoded as UTF-8. + /// + /// If you wish to convert from and to Unicode code points, see the + /// [`to-unicode`]($str.to-unicode) and [`from-unicode`]($str.from-unicode) + /// functions. + /// + /// ```example + /// #str(10) \ + /// #str(4000, base: 16) \ + /// #str(2.7) \ + /// #str(1e8) \ + /// #str(<intro>) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a string. + value: ToStr, + /// The base (radix) to display integers in, between 2 and 36. + #[named] + #[default(Spanned::new(10, Span::detached()))] + base: Spanned<i64>, + ) -> SourceResult<Str> { + Ok(match value { + ToStr::Str(s) => { + if base.v != 10 { + bail!(base.span, "base is only supported for integers"); + } + s + } + ToStr::Int(n) => { + if base.v < 2 || base.v > 36 { + bail!(base.span, "base must be between 2 and 36"); + } + repr::format_int_with_base(n, base.v).into() + } + }) + } + + /// The length of the string in UTF-8 encoded bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Extracts the first grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn first(&self) -> StrResult<Str> { + self.0 + .graphemes(true) + .next() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extracts the last grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn last(&self) -> StrResult<Str> { + self.0 + .graphemes(true) + .next_back() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extracts the first grapheme cluster after the specified index. Returns + /// the default value if the index is out of bounds or fails with an error + /// if no default value was specified. + #[func] + pub fn at( + &self, + /// The byte index. If negative, indexes from the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + let len = self.len(); + self.locate_opt(index)? + .and_then(|i| self.0[i..].graphemes(true).next().map(|s| s.into_value())) + .or(default) + .ok_or_else(|| no_default_and_out_of_bounds(index, len)) + } + + /// Extracts a substring of the string. + /// Fails with an error if the start or end index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start byte index (inclusive). If negative, indexes from the + /// back. + start: i64, + /// The end byte index (exclusive). If omitted, the whole slice until + /// the end of the string is extracted. If negative, indexes from the + /// back. + #[default] + end: Option<i64>, + /// The number of bytes to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Str> { + let end = end.or(count.map(|c| start + c)).unwrap_or(self.len() as i64); + let start = self.locate(start)?; + let end = self.locate(end)?.max(start); + Ok(self.0[start..end].into()) + } + + /// Returns the grapheme clusters of the string as an array of substrings. + #[func] + pub fn clusters(&self) -> Array { + self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() + } + + /// Returns the Unicode codepoints of the string as an array of substrings. + #[func] + pub fn codepoints(&self) -> Array { + self.chars().map(|c| Value::Str(c.into())).collect() + } + + /// Converts a character into its corresponding code point. + /// + /// ```example + /// #"a".to-unicode() \ + /// #("a\u{0300}" + /// .codepoints() + /// .map(str.to-unicode)) + /// ``` + #[func] + pub fn to_unicode( + /// The character that should be converted. + character: char, + ) -> u32 { + character as u32 + } + + /// Converts a unicode code point into its corresponding string. + /// + /// ```example + /// #str.from-unicode(97) + /// ``` + #[func] + pub fn from_unicode( + /// The code point that should be converted. + value: u32, + ) -> StrResult<Str> { + let c: char = value + .try_into() + .map_err(|_| eco_format!("{value:#x} is not a valid codepoint"))?; + Ok(c.into()) + } + + /// Whether the string contains the specified pattern. + /// + /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` + /// instead of `{"abcd".contains("bc")}`. + #[func] + pub fn contains( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()), + StrPattern::Regex(re) => re.is_match(self), + } + } + + /// Whether the string starts with the specified pattern. + #[func] + pub fn starts_with( + &self, + /// The pattern the string might start with. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), + StrPattern::Regex(re) => re.find(self).is_some_and(|m| m.start() == 0), + } + } + + /// Whether the string ends with the specified pattern. + #[func] + pub fn ends_with( + &self, + /// The pattern the string might end with. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), + StrPattern::Regex(re) => { + let mut start_byte = 0; + while let Some(mat) = re.find_at(self, start_byte) { + if mat.end() == self.0.len() { + return true; + } + + // There might still be a match overlapping this one, so + // restart at the next code point. + let Some(c) = self[mat.start()..].chars().next() else { break }; + start_byte = mat.start() + c.len_utf8(); + } + false + } + } + } + + /// Searches for the specified pattern in the string and returns the first + /// match as a string or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<Str> { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()).then_some(pat), + StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), + } + } + + /// Searches for the specified pattern in the string and returns the index + /// of the first match as an integer or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<usize> { + match pattern { + StrPattern::Str(pat) => self.0.find(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map(|m| m.start()), + } + } + + /// Searches for the specified pattern in the string and returns a + /// dictionary with details about the first match or `{none}` if there is no + /// match. + /// + /// The returned dictionary has the following keys: + /// - `start`: The start offset of the match + /// - `end`: The end offset of the match + /// - `text`: The text that matched. + /// - `captures`: An array containing a string for each matched capturing + /// group. The first item of the array contains the first matched + /// capturing, not the whole match! This is empty unless the `pattern` was + /// a regex with capturing groups. + #[func] + pub fn match_( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<Dict> { + match pattern { + StrPattern::Str(pat) => { + self.0.match_indices(pat.as_str()).next().map(match_to_dict) + } + StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), + } + } + + /// Searches for the specified pattern in the string and returns an array of + /// dictionaries with details about all matches. For details about the + /// returned dictionaries, see above. + #[func] + pub fn matches( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Array { + match pattern { + StrPattern::Str(pat) => self + .0 + .match_indices(pat.as_str()) + .map(match_to_dict) + .map(Value::Dict) + .collect(), + StrPattern::Regex(re) => re + .captures_iter(self) + .map(captures_to_dict) + .map(Value::Dict) + .collect(), + } + } + + /// Replace at most `count` occurrences of the given pattern with a + /// replacement string or function (beginning from the start). If no count + /// is given, all occurrences are replaced. + #[func] + pub fn replace( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The pattern to search for. + pattern: StrPattern, + /// The string to replace the matches with or a function that gets a + /// dictionary for each match and can return individual replacement + /// strings. + replacement: Replacement, + /// If given, only the first `count` matches of the pattern are placed. + #[named] + count: Option<usize>, + ) -> SourceResult<Str> { + // Heuristic: Assume the new string is about the same length as + // the current string. + let mut output = EcoString::with_capacity(self.as_str().len()); + + // Replace one match of a pattern with the replacement. + let mut last_match = 0; + let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> { + // Push everything until the match. + output.push_str(&self[last_match..range.start]); + last_match = range.end; + + // Determine and push the replacement. + match &replacement { + Replacement::Str(s) => output.push_str(s), + Replacement::Func(func) => { + let piece = func + .call(engine, context, [dict])? + .cast::<Str>() + .at(func.span())?; + output.push_str(&piece); + } + } + + Ok(()) + }; + + // Iterate over the matches of the `pattern`. + let count = count.unwrap_or(usize::MAX); + match &pattern { + StrPattern::Str(pat) => { + for m in self.match_indices(pat.as_str()).take(count) { + let (start, text) = m; + handle_match(start..start + text.len(), match_to_dict(m))?; + } + } + StrPattern::Regex(re) => { + for caps in re.captures_iter(self).take(count) { + // Extract the entire match over all capture groups. + let m = caps.get(0).unwrap(); + handle_match(m.start()..m.end(), captures_to_dict(caps))?; + } + } + } + + // Push the remainder. + output.push_str(&self[last_match..]); + Ok(output.into()) + } + + /// Removes matches of a pattern from one or both sides of the string, once or + /// repeatedly and returns the resulting string. + #[func] + pub fn trim( + &self, + /// The pattern to search for. If `{none}`, trims white spaces. + #[default] + pattern: Option<StrPattern>, + /// Can be `{start}` or `{end}` to only trim the start or end of the + /// string. If omitted, both sides are trimmed. + #[named] + at: Option<StrSide>, + /// Whether to repeatedly removes matches of the pattern or just once. + /// Defaults to `{true}`. + #[named] + #[default(true)] + repeat: bool, + ) -> Str { + let mut start = matches!(at, Some(StrSide::Start) | None); + let end = matches!(at, Some(StrSide::End) | None); + + let trimmed = match pattern { + None => match at { + None => self.0.trim(), + Some(StrSide::Start) => self.0.trim_start(), + Some(StrSide::End) => self.0.trim_end(), + }, + Some(StrPattern::Str(pat)) => { + let pat = pat.as_str(); + let mut s = self.as_str(); + if repeat { + if start { + s = s.trim_start_matches(pat); + } + if end { + s = s.trim_end_matches(pat); + } + } else { + if start { + s = s.strip_prefix(pat).unwrap_or(s); + } + if end { + s = s.strip_suffix(pat).unwrap_or(s); + } + } + s + } + Some(StrPattern::Regex(re)) => { + let s = self.as_str(); + let mut last = None; + let mut range = 0..s.len(); + + for m in re.find_iter(s) { + // Does this match follow directly after the last one? + let consecutive = last == Some(m.start()); + + // As long as we're at the beginning or in a consecutive run + // of matches, and we're still trimming at the start, trim. + start &= m.start() == 0 || consecutive; + if start { + range.start = m.end(); + start &= repeat; + } + + // Reset end trim if we aren't consecutive anymore or aren't + // repeating. + if end && (!consecutive || !repeat) { + range.end = m.start(); + } + + last = Some(m.end()); + } + + // Is the last match directly at the end? + if last.is_some_and(|last| last < s.len()) { + range.end = s.len(); + } + + &s[range.start..range.start.max(range.end)] + } + }; + + trimmed.into() + } + + /// Splits a string at matches of a specified pattern and returns an array + /// of the resulting parts. + #[func] + pub fn split( + &self, + /// The pattern to split at. Defaults to whitespace. + #[default] + pattern: Option<StrPattern>, + ) -> Array { + let s = self.as_str(); + match pattern { + None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), + Some(StrPattern::Str(pat)) => { + s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() + } + Some(StrPattern::Regex(re)) => { + re.split(s).map(|v| Value::Str(v.into())).collect() + } + } + } + + /// Reverse the string. + #[func(title = "Reverse")] + pub fn rev(&self) -> Str { + let mut s = EcoString::with_capacity(self.0.len()); + for grapheme in self.as_str().graphemes(true).rev() { + s.push_str(grapheme); + } + s.into() + } +} + +impl Deref for Str { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Debug for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self.as_str(), f) + } +} + +impl Display for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self.as_str(), f) + } +} + +impl Repr for Str { + fn repr(&self) -> EcoString { + self.as_ref().repr() + } +} + +impl Repr for EcoString { + fn repr(&self) -> EcoString { + self.as_ref().repr() + } +} + +impl Repr for str { + fn repr(&self) -> EcoString { + let mut r = EcoString::with_capacity(self.len() + 2); + r.push('"'); + for c in self.chars() { + match c { + '\0' => r.push_str(r"\u{0}"), + '\'' => r.push('\''), + '"' => r.push_str(r#"\""#), + _ => c.escape_debug().for_each(|c| r.push(c)), + } + } + r.push('"'); + r + } +} + +impl Add for Str { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Str { + fn add_assign(&mut self, rhs: Self) { + self.0.push_str(rhs.as_str()); + } +} + +impl AsRef<str> for Str { + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow<str> for Str { + fn borrow(&self) -> &str { + self + } +} + +impl From<char> for Str { + fn from(c: char) -> Self { + Self(c.into()) + } +} + +impl From<&str> for Str { + fn from(s: &str) -> Self { + Self(s.into()) + } +} + +impl From<EcoString> for Str { + fn from(s: EcoString) -> Self { + Self(s) + } +} + +impl From<String> for Str { + fn from(s: String) -> Self { + Self(s.into()) + } +} + +impl From<Cow<'_, str>> for Str { + fn from(s: Cow<str>) -> Self { + Self(s.into()) + } +} + +impl FromIterator<char> for Str { + fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl From<Str> for EcoString { + fn from(str: Str) -> Self { + str.0 + } +} + +impl From<Str> for String { + fn from(s: Str) -> Self { + s.0.into() + } +} + +cast! { + char, + self => Value::Str(self.into()), + string: Str => { + let mut chars = string.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => c, + _ => bail!("expected exactly one character"), + } + }, +} + +cast! { + &str, + self => Value::Str(self.into()), +} + +cast! { + EcoString, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +cast! { + PicoStr, + self => Value::Str(self.resolve().into()), + v: Str => v.as_str().into(), +} + +cast! { + String, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +/// A value that can be cast to a string. +pub enum ToStr { + /// A string value ready to be used as-is. + Str(Str), + /// An integer about to be formatted in a given base. + Int(i64), +} + +cast! { + ToStr, + v: i64 => Self::Int(v), + v: f64 => Self::Str(repr::display_float(v).into()), + v: Decimal => Self::Str(format_str!("{}", v)), + v: Version => Self::Str(format_str!("{}", v)), + v: Bytes => Self::Str( + std::str::from_utf8(&v) + .map_err(|_| "bytes are not valid utf-8")? + .into() + ), + v: Label => Self::Str(v.as_str().into()), + v: Type => Self::Str(v.long_name().into()), + v: Str => Self::Str(v), +} + +/// Convert an item of std's `match_indices` to a dictionary. +fn match_to_dict((start, text): (usize, &str)) -> Dict { + dict! { + "start" => start, + "end" => start + text.len(), + "text" => text, + "captures" => Array::new(), + } +} + +/// Convert regex captures to a dictionary. +fn captures_to_dict(cap: regex::Captures) -> Dict { + let m = cap.get(0).expect("missing first match"); + dict! { + "start" => m.start(), + "end" => m.end(), + "text" => m.as_str(), + "captures" => cap.iter() + .skip(1) + .map(|opt| opt.map_or(Value::None, |m| m.as_str().into_value())) + .collect::<Array>(), + } +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("string index out of bounds (index: {}, len: {})", index, len) +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn no_default_and_out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len) +} + +/// The char boundary access error message. +#[cold] +fn not_a_char_boundary(index: i64) -> EcoString { + eco_format!("string index {} is not a character boundary", index) +} + +/// The error message when the string is empty. +#[cold] +fn string_is_empty() -> EcoString { + "string is empty".into() +} + +/// A regular expression. +/// +/// Can be used as a [show rule selector]($styling/#show-rules) and with +/// [string methods]($str) like `find`, `split`, and `replace`. +/// +/// [See here](https://docs.rs/regex/latest/regex/#syntax) for a specification +/// of the supported syntax. +/// +/// # Example +/// ```example +/// // Works with string methods. +/// #"a,b;c".split(regex("[,;]")) +/// +/// // Works with show rules. +/// #show regex("\d+"): set text(red) +/// +/// The numbers 1 to 10. +/// ``` +#[ty(scope)] +#[derive(Debug, Clone)] +pub struct Regex(regex::Regex); + +impl Regex { + /// Create a new regular expression. + pub fn new(re: &str) -> StrResult<Self> { + regex::Regex::new(re).map(Self).map_err(|err| eco_format!("{err}")) + } +} + +#[scope] +impl Regex { + /// Create a regular expression from a string. + #[func(constructor)] + pub fn construct( + /// The regular expression as a string. + /// + /// Most regex escape sequences just work because they are not valid Typst + /// escape sequences. To produce regex escape sequences that are also valid in + /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim + /// backslash, you would need to write `{regex("\\\\")}`. + /// + /// If you need many escape sequences, you can also create a raw element + /// and extract its text to use it for your regular expressions: + /// ```{regex(`\d+\.\d+\.\d+`.text)}```. + regex: Spanned<Str>, + ) -> SourceResult<Regex> { + Self::new(®ex.v).at(regex.span) + } +} + +impl Deref for Regex { + type Target = regex::Regex; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Repr for Regex { + fn repr(&self) -> EcoString { + eco_format!("regex({})", self.0.as_str().repr()) + } +} + +impl PartialEq for Regex { + fn eq(&self, other: &Self) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Hash for Regex { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.as_str().hash(state); + } +} + +/// A pattern which can be searched for in a string. +#[derive(Debug, Clone)] +pub enum StrPattern { + /// Just a string. + Str(Str), + /// A regular expression. + Regex(Regex), +} + +cast! { + StrPattern, + self => match self { + Self::Str(v) => v.into_value(), + Self::Regex(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Regex => Self::Regex(v), +} + +/// A side of a string. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum StrSide { + /// The logical start of the string, may be left or right depending on the + /// language. + Start, + /// The logical end of the string. + End, +} + +cast! { + StrSide, + v: Alignment => match v { + Alignment::START => Self::Start, + Alignment::END => Self::End, + _ => bail!("expected either `start` or `end`"), + }, +} + +/// A replacement for a matched [`Str`] +pub enum Replacement { + /// A string a match is replaced with. + Str(Str), + /// Function of type Dict -> Str (see `captures_to_dict` or `match_to_dict`) + /// whose output is inserted for the match. + Func(Func), +} + +cast! { + Replacement, + self => match self { + Self::Str(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Func => Self::Func(v) +} diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs new file mode 100644 index 00000000..af4909e5 --- /dev/null +++ b/crates/typst-library/src/foundations/styles.rs @@ -0,0 +1,1034 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::{mem, ptr}; + +use comemo::{Track, Tracked}; +use ecow::{eco_vec, EcoString, EcoVec}; +use smallvec::SmallVec; +use typst_syntax::Span; +use typst_utils::LazyHash; + +use crate::diag::{warning, SourceResult, Trace, Tracepoint}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr, + Selector, Show, +}; +use crate::introspection::Locatable; +use crate::text::{FontFamily, FontList, TextElem}; + +/// Provides access to active styles. +/// +/// **Deprecation planned.** Use [context] instead. +/// +/// ```example +/// #let thing(body) = style(styles => { +/// let size = measure(body, styles) +/// [Width of "#body" is #size.width] +/// }) +/// +/// #thing[Hey] \ +/// #thing[Welcome] +/// ``` +#[func] +pub fn style( + /// The engine. + engine: &mut Engine, + /// The call site span. + span: Span, + /// A function to call with the styles. Its return value is displayed + /// in the document. + /// + /// This function is called once for each time the content returned by + /// `style` appears in the document. That makes it possible to generate + /// content that depends on the style context it appears in. + func: Func, +) -> Content { + engine.sink.warn(warning!( + span, "`style` is deprecated"; + hint: "use a `context` expression instead" + )); + + StyleElem::new(func).pack().spanned(span) +} + +/// Executes a style access. +#[elem(Locatable, Show)] +struct StyleElem { + /// The function to call with the styles. + #[required] + func: Func, +} + +impl Show for Packed<StyleElem> { + #[typst_macros::time(name = "style", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let context = Context::new(self.location(), Some(styles)); + Ok(self + .func() + .call(engine, context.track(), [styles.to_map()])? + .display()) + } +} + +/// A list of style properties. +#[ty(cast)] +#[derive(Default, PartialEq, Clone, Hash)] +pub struct Styles(EcoVec<LazyHash<Style>>); + +impl Styles { + /// Create a new, empty style list. + pub const fn new() -> Self { + Self(EcoVec::new()) + } + + /// Whether this contains no styles. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Iterate over the contained styles. + pub fn iter(&self) -> impl Iterator<Item = &Style> { + self.0.iter().map(|style| &**style) + } + + /// Iterate over the contained styles. + pub fn as_slice(&self) -> &[LazyHash<Style>] { + self.0.as_slice() + } + + /// Set an inner value for a style property. + /// + /// If the property needs folding and the value is already contained in the + /// style map, `self` contributes the outer values and `value` is the inner + /// one. + pub fn set(&mut self, style: impl Into<Style>) { + self.0.push(LazyHash::new(style.into())); + } + + /// Remove the style that was last set. + pub fn unset(&mut self) { + self.0.pop(); + } + + /// Apply outer styles. Like [`chain`](StyleChain::chain), but in-place. + pub fn apply(&mut self, mut outer: Self) { + outer.0.extend(mem::take(self).0); + *self = outer; + } + + /// Apply one outer styles. + pub fn apply_one(&mut self, outer: Style) { + self.0.insert(0, LazyHash::new(outer)); + } + + /// Add an origin span to all contained properties. + pub fn spanned(mut self, span: Span) -> Self { + for entry in self.0.make_mut() { + if let Style::Property(property) = &mut **entry { + property.span = span; + } + } + self + } + + /// Marks the styles as having been applied outside of any show rule. + pub fn outside(mut self) -> Self { + for entry in self.0.make_mut() { + match &mut **entry { + Style::Property(property) => property.outside = true, + Style::Recipe(recipe) => recipe.outside = true, + _ => {} + } + } + self + } + + /// Marks the styles as being allowed to be lifted up to the page level. + pub fn liftable(mut self) -> Self { + for entry in self.0.make_mut() { + if let Style::Property(property) = &mut **entry { + property.liftable = true; + } + } + self + } + + /// Whether there is a style for the given field of the given element. + pub fn has<T: NativeElement>(&self, field: u8) -> bool { + let elem = T::elem(); + self.0 + .iter() + .filter_map(|style| style.property()) + .any(|property| property.is_of(elem) && property.id == field) + } + + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { + self.set(TextElem::set_font(FontList( + std::iter::once(preferred) + .chain(TextElem::font_in(existing).into_iter().cloned()) + .collect(), + ))); + } +} + +impl From<LazyHash<Style>> for Styles { + fn from(style: LazyHash<Style>) -> Self { + Self(eco_vec![style]) + } +} + +impl From<Style> for Styles { + fn from(style: Style) -> Self { + Self(eco_vec![LazyHash::new(style)]) + } +} + +impl IntoIterator for Styles { + type Item = LazyHash<Style>; + type IntoIter = ecow::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<LazyHash<Style>> for Styles { + fn from_iter<T: IntoIterator<Item = LazyHash<Style>>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl Debug for Styles { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Styles ")?; + f.debug_list().entries(&self.0).finish() + } +} + +impl Repr for Styles { + fn repr(&self) -> EcoString { + "..".into() + } +} + +/// A single style property or recipe. +#[derive(Clone, Hash)] +pub enum Style { + /// A style property originating from a set rule or constructor. + Property(Property), + /// A show rule recipe. + Recipe(Recipe), + /// Disables a specific show rule recipe. + /// + /// Note: This currently only works for regex recipes since it's the only + /// place we need it for the moment. Normal show rules use guards directly + /// on elements instead. + Revocation(RecipeIndex), +} + +impl Style { + /// If this is a property, return it. + pub fn property(&self) -> Option<&Property> { + match self { + Self::Property(property) => Some(property), + _ => None, + } + } + + /// If this is a recipe, return it. + pub fn recipe(&self) -> Option<&Recipe> { + match self { + Self::Recipe(recipe) => Some(recipe), + _ => None, + } + } + + /// The style's span, if any. + pub fn span(&self) -> Span { + match self { + Self::Property(property) => property.span, + Self::Recipe(recipe) => recipe.span, + Self::Revocation(_) => Span::detached(), + } + } + + /// Returns `Some(_)` with an optional span if this style is for + /// the given element. + pub fn element(&self) -> Option<Element> { + match self { + Style::Property(property) => Some(property.elem), + Style::Recipe(recipe) => match recipe.selector { + Some(Selector::Elem(elem, _)) => Some(elem), + _ => None, + }, + Style::Revocation(_) => None, + } + } + + /// Whether the style is allowed to be lifted up to the page level. Only + /// true for styles originating from set rules. + pub fn liftable(&self) -> bool { + match self { + Self::Property(property) => property.liftable, + Self::Recipe(_) => true, + Self::Revocation(_) => false, + } + } + + /// Whether the style was applied outside of any show rule. This is set + /// during realization. + pub fn outside(&self) -> bool { + match self { + Self::Property(property) => property.outside, + Self::Recipe(recipe) => recipe.outside, + Self::Revocation(_) => false, + } + } + + /// Turn this style into prehashed style. + pub fn wrap(self) -> LazyHash<Style> { + LazyHash::new(self) + } +} + +impl Debug for Style { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Property(property) => property.fmt(f), + Self::Recipe(recipe) => recipe.fmt(f), + Self::Revocation(guard) => guard.fmt(f), + } + } +} + +impl From<Property> for Style { + fn from(property: Property) -> Self { + Self::Property(property) + } +} + +impl From<Recipe> for Style { + fn from(recipe: Recipe) -> Self { + Self::Recipe(recipe) + } +} + +/// A style property originating from a set rule or constructor. +#[derive(Clone, Hash)] +pub struct Property { + /// The element the property belongs to. + elem: Element, + /// The property's ID. + id: u8, + /// The property's value. + value: Block, + /// The span of the set rule the property stems from. + span: Span, + /// Whether the property is allowed to be lifted up to the page level. + liftable: bool, + /// Whether the property was applied outside of any show rule. + outside: bool, +} + +impl Property { + /// Create a new property from a key-value pair. + pub fn new<E, T>(id: u8, value: T) -> Self + where + E: NativeElement, + T: Debug + Clone + Hash + Send + Sync + 'static, + { + Self { + elem: E::elem(), + id, + value: Block::new(value), + span: Span::detached(), + liftable: false, + outside: false, + } + } + + /// Whether this property is the given one. + pub fn is(&self, elem: Element, id: u8) -> bool { + self.elem == elem && self.id == id + } + + /// Whether this property belongs to the given element. + pub fn is_of(&self, elem: Element) -> bool { + self.elem == elem + } + + /// Turn this property into prehashed style. + pub fn wrap(self) -> LazyHash<Style> { + Style::Property(self).wrap() + } +} + +impl Debug for Property { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "Set({}.{}: ", + self.elem.name(), + self.elem.field_name(self.id).unwrap() + )?; + self.value.fmt(f)?; + write!(f, ")") + } +} + +/// A block storage for storing style values. +/// +/// We're using a `Box` since values will either be contained in an `Arc` and +/// therefore already on the heap or they will be small enough that we can just +/// clone them. +#[derive(Hash)] +struct Block(Box<dyn Blockable>); + +impl Block { + /// Creates a new block. + fn new<T: Blockable>(value: T) -> Self { + Self(Box::new(value)) + } + + /// Downcasts the block to the specified type. + fn downcast<T: 'static>(&self) -> Option<&T> { + self.0.as_any().downcast_ref() + } +} + +impl Debug for Block { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Clone for Block { + fn clone(&self) -> Self { + self.0.dyn_clone() + } +} + +/// A value that can be stored in a block. +/// +/// Auto derived for all types that implement [`Any`], [`Clone`], [`Hash`], +/// [`Debug`], [`Send`] and [`Sync`]. +trait Blockable: Debug + Send + Sync + 'static { + /// Equivalent to `downcast_ref` for the block. + fn as_any(&self) -> &dyn Any; + + /// Equivalent to [`Hash`] for the block. + fn dyn_hash(&self, state: &mut dyn Hasher); + + /// Equivalent to [`Clone`] for the block. + fn dyn_clone(&self) -> Block; +} + +impl<T: Debug + Clone + Hash + Send + Sync + 'static> Blockable for T { + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + // Also hash the TypeId since values with different types but + // equal data should be different. + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } + + fn dyn_clone(&self) -> Block { + Block(Box::new(self.clone())) + } +} + +impl Hash for dyn Blockable { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// A show rule recipe. +#[derive(Clone, PartialEq, Hash)] +pub struct Recipe { + /// Determines whether the recipe applies to an element. + /// + /// If this is `None`, then this recipe is from a show rule with + /// no selector (`show: rest => ...`), which is [eagerly applied][Content::styled_with_recipe] + /// to the rest of the content in the scope. + selector: Option<Selector>, + /// The transformation to perform on the match. + transform: Transformation, + /// The span that errors are reported with. + span: Span, + /// Relevant properties of the kind of construct the style originated from + /// and where it was applied. + outside: bool, +} + +impl Recipe { + /// Create a new recipe from a key-value pair. + pub fn new( + selector: Option<Selector>, + transform: Transformation, + span: Span, + ) -> Self { + Self { selector, transform, span, outside: false } + } + + /// The recipe's selector. + pub fn selector(&self) -> Option<&Selector> { + self.selector.as_ref() + } + + /// The recipe's transformation. + pub fn transform(&self) -> &Transformation { + &self.transform + } + + /// The recipe's span. + pub fn span(&self) -> Span { + self.span + } + + /// Apply the recipe to the given content. + pub fn apply( + &self, + engine: &mut Engine, + context: Tracked<Context>, + content: Content, + ) -> SourceResult<Content> { + let mut content = match &self.transform { + Transformation::Content(content) => content.clone(), + Transformation::Func(func) => { + let mut result = func.call(engine, context, [content.clone()]); + if self.selector.is_some() { + let point = || Tracepoint::Show(content.func().name().into()); + result = result.trace(engine.world, point, content.span()); + } + result?.display() + } + Transformation::Style(styles) => content.styled_with_map(styles.clone()), + }; + if content.span().is_detached() { + content = content.spanned(self.span); + } + Ok(content) + } +} + +impl Debug for Recipe { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Show(")?; + if let Some(selector) = &self.selector { + selector.fmt(f)?; + f.write_str(", ")?; + } + self.transform.fmt(f) + } +} + +/// Identifies a show rule recipe from the top of the chain. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct RecipeIndex(pub usize); + +/// A show rule transformation that can be applied to a match. +#[derive(Clone, PartialEq, Hash)] +pub enum Transformation { + /// Replacement content. + Content(Content), + /// A function to apply to the match. + Func(Func), + /// Apply styles to the content. + Style(Styles), +} + +impl Debug for Transformation { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Content(content) => content.fmt(f), + Self::Func(func) => func.fmt(f), + Self::Style(styles) => styles.fmt(f), + } + } +} + +cast! { + Transformation, + content: Content => Self::Content(content), + func: Func => Self::Func(func), +} + +/// A chain of styles, similar to a linked list. +/// +/// A style chain allows to combine properties from multiple style lists in a +/// element hierarchy in a non-allocating way. Rather than eagerly merging the +/// lists, each access walks the hierarchy from the innermost to the outermost +/// map, trying to find a match and then folding it with matches further up the +/// chain. +#[derive(Default, Clone, Copy, Hash)] +pub struct StyleChain<'a> { + /// The first link of this chain. + head: &'a [LazyHash<Style>], + /// The remaining links in the chain. + tail: Option<&'a Self>, +} + +impl<'a> StyleChain<'a> { + /// Start a new style chain with root styles. + pub fn new(root: &'a Styles) -> Self { + Self { head: &root.0, tail: None } + } + + /// Make the given chainable the first link of this chain. + /// + /// The resulting style chain contains styles from `local` as well as + /// `self`. The ones from `local` take precedence over the ones from + /// `self`. For folded properties `local` contributes the inner value. + pub fn chain<'b, C>(&'b self, local: &'b C) -> StyleChain<'b> + where + C: Chainable + ?Sized, + { + Chainable::chain(local, self) + } + + /// Cast the first value for the given property in the chain. + pub fn get<T: Clone + 'static>( + self, + func: Element, + id: u8, + inherent: Option<&T>, + default: impl Fn() -> T, + ) -> T { + self.properties::<T>(func, id, inherent) + .next() + .cloned() + .unwrap_or_else(default) + } + + /// Cast the first value for the given property in the chain, + /// returning a borrowed value. + pub fn get_ref<T: 'static>( + self, + func: Element, + id: u8, + inherent: Option<&'a T>, + default: impl Fn() -> &'a T, + ) -> &'a T { + self.properties::<T>(func, id, inherent) + .next() + .unwrap_or_else(default) + } + + /// Cast the first value for the given property in the chain, taking + /// `Fold` implementations into account. + pub fn get_folded<T: Fold + Clone + 'static>( + self, + func: Element, + id: u8, + inherent: Option<&T>, + default: impl Fn() -> T, + ) -> T { + fn next<T: Fold>( + mut values: impl Iterator<Item = T>, + default: &impl Fn() -> T, + ) -> T { + values + .next() + .map(|value| value.fold(next(values, default))) + .unwrap_or_else(default) + } + next(self.properties::<T>(func, id, inherent).cloned(), &default) + } + + /// Iterate over all values for the given property in the chain. + fn properties<T: 'static>( + self, + func: Element, + id: u8, + inherent: Option<&'a T>, + ) -> impl Iterator<Item = &'a T> { + inherent.into_iter().chain( + self.entries() + .filter_map(|style| style.property()) + .filter(move |property| property.is(func, id)) + .map(|property| &property.value) + .map(move |value| { + value.downcast().unwrap_or_else(|| { + panic!( + "attempted to read a value of a different type than was written {}.{}: {:?}", + func.name(), + func.field_name(id).unwrap(), + value + ) + }) + }), + ) + } + + /// Iterate over the entries of the chain. + pub fn entries(self) -> Entries<'a> { + Entries { inner: [].as_slice().iter(), links: self.links() } + } + + /// Iterate over the recipes in the chain. + pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> { + self.entries().filter_map(|style| style.recipe()) + } + + /// Iterate over the links of the chain. + pub fn links(self) -> Links<'a> { + Links(Some(self)) + } + + /// Convert to a style map. + pub fn to_map(self) -> Styles { + let mut styles: EcoVec<_> = self.entries().cloned().collect(); + styles.make_mut().reverse(); + Styles(styles) + } + + /// Build owned styles from the suffix (all links beyond the `len`) of the + /// chain. + pub fn suffix(self, len: usize) -> Styles { + let mut styles = EcoVec::new(); + let take = self.links().count().saturating_sub(len); + for link in self.links().take(take) { + styles.extend(link.iter().cloned().rev()); + } + styles.make_mut().reverse(); + Styles(styles) + } + + /// Remove the last link from the chain. + pub fn pop(&mut self) { + *self = self.tail.copied().unwrap_or_default(); + } + + /// Determine the shared trunk of a collection of style chains. + pub fn trunk(iter: impl IntoIterator<Item = Self>) -> Option<Self> { + // Determine shared style depth and first span. + let mut iter = iter.into_iter(); + let mut trunk = iter.next()?; + let mut depth = trunk.links().count(); + + for mut chain in iter { + let len = chain.links().count(); + if len < depth { + for _ in 0..depth - len { + trunk.pop(); + } + depth = len; + } else if len > depth { + for _ in 0..len - depth { + chain.pop(); + } + } + + while depth > 0 && chain != trunk { + trunk.pop(); + chain.pop(); + depth -= 1; + } + } + + Some(trunk) + } +} + +impl Debug for StyleChain<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("StyleChain ")?; + f.debug_list() + .entries(self.entries().collect::<Vec<_>>().into_iter().rev()) + .finish() + } +} + +impl PartialEq for StyleChain<'_> { + fn eq(&self, other: &Self) -> bool { + ptr::eq(self.head, other.head) + && match (self.tail, other.tail) { + (Some(a), Some(b)) => ptr::eq(a, b), + (None, None) => true, + _ => false, + } + } +} + +/// Things that can be attached to a style chain. +pub trait Chainable { + /// Attach `self` as the first link of the chain. + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a>; +} + +impl Chainable for LazyHash<Style> { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + StyleChain { + head: std::slice::from_ref(self), + tail: Some(outer), + } + } +} + +impl Chainable for [LazyHash<Style>] { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + if self.is_empty() { + *outer + } else { + StyleChain { head: self, tail: Some(outer) } + } + } +} + +impl<const N: usize> Chainable for [LazyHash<Style>; N] { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + Chainable::chain(self.as_slice(), outer) + } +} + +impl Chainable for Styles { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + Chainable::chain(self.0.as_slice(), outer) + } +} + +/// An iterator over the entries in a style chain. +pub struct Entries<'a> { + inner: std::slice::Iter<'a, LazyHash<Style>>, + links: Links<'a>, +} + +impl<'a> Iterator for Entries<'a> { + type Item = &'a LazyHash<Style>; + + fn next(&mut self) -> Option<Self::Item> { + loop { + if let Some(entry) = self.inner.next_back() { + return Some(entry); + } + + match self.links.next() { + Some(next) => self.inner = next.iter(), + None => return None, + } + } + } +} + +/// An iterator over the links of a style chain. +pub struct Links<'a>(Option<StyleChain<'a>>); + +impl<'a> Iterator for Links<'a> { + type Item = &'a [LazyHash<Style>]; + + fn next(&mut self) -> Option<Self::Item> { + let StyleChain { head, tail } = self.0?; + self.0 = tail.copied(); + Some(head) + } +} + +/// A sequence of elements with associated styles. +#[derive(Clone, PartialEq, Hash)] +pub struct StyleVec { + /// The elements themselves. + elements: EcoVec<Content>, + /// A run-length encoded list of style lists. + /// + /// Each element is a (styles, count) pair. Any elements whose + /// style falls after the end of this list is considered to + /// have an empty style list. + styles: EcoVec<(Styles, usize)>, +} + +impl StyleVec { + /// Create a style vector from an unstyled vector content. + pub fn wrap(elements: EcoVec<Content>) -> Self { + Self { elements, styles: EcoVec::new() } + } + + /// Create a `StyleVec` from a list of content with style chains. + pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut elements = EcoVec::with_capacity(buf.len()); + let mut styles = EcoVec::<(Styles, usize)>::new(); + let mut last: Option<(StyleChain<'a>, usize)> = None; + + for &(element, chain) in buf { + elements.push(element.clone()); + + if let Some((prev, run)) = &mut last { + if chain == *prev { + *run += 1; + } else { + styles.push((prev.suffix(depth), *run)); + last = Some((chain, 1)); + } + } else { + last = Some((chain, 1)); + } + } + + if let Some((last, run)) = last { + let skippable = styles.is_empty() && last == trunk; + if !skippable { + styles.push((last.suffix(depth), run)); + } + } + + (StyleVec { elements, styles }, trunk) + } + + /// Whether there are no elements. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + /// The number of elements. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Iterate over the contained content and style chains. + pub fn iter<'a>( + &'a self, + outer: &'a StyleChain<'_>, + ) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> { + static EMPTY: Styles = Styles::new(); + self.elements + .iter() + .zip( + self.styles + .iter() + .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) + .chain(std::iter::repeat(&EMPTY)), + ) + .map(|(element, local)| (element, outer.chain(local))) + } + + /// Get a style property, but only if it is the same for all children of the + /// style vector. + pub fn shared_get<T: PartialEq>( + &self, + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, + ) -> Option<T> { + let value = getter(styles); + self.styles + .iter() + .all(|(local, _)| getter(styles.chain(local)) == value) + .then_some(value) + } +} + +impl Debug for StyleVec { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.elements).finish() + } +} + +/// A property that is resolved with other properties from the style chain. +pub trait Resolve { + /// The type of the resolved output. + type Output; + + /// Resolve the value using the style chain. + fn resolve(self, styles: StyleChain) -> Self::Output; +} + +impl<T: Resolve> Resolve for Option<T> { + type Output = Option<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +/// A property that is folded to determine its final value. +/// +/// In the example below, the chain of stroke values is folded into a single +/// value: `4pt + red`. +/// +/// ```example +/// #set rect(stroke: red) +/// #set rect(stroke: 4pt) +/// #rect() +/// ``` +pub trait Fold { + /// Fold this inner value with an outer folded value. + fn fold(self, outer: Self) -> Self; +} + +impl Fold for bool { + fn fold(self, _: Self) -> Self { + self + } +} + +impl<T: Fold> Fold for Option<T> { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Some(inner), Some(outer)) => Some(inner.fold(outer)), + // An explicit `None` should be respected, thus we don't do + // `inner.or(outer)`. + (inner, _) => inner, + } + } +} + +impl<T> Fold for Vec<T> { + fn fold(self, mut outer: Self) -> Self { + outer.extend(self); + outer + } +} + +impl<T, const N: usize> Fold for SmallVec<[T; N]> { + fn fold(self, mut outer: Self) -> Self { + outer.extend(self); + outer + } +} + +/// A variant of fold for foldable optional (`Option<T>`) values where an inner +/// `None` value isn't respected (contrary to `Option`'s usual `Fold` +/// implementation, with which folding with an inner `None` always returns +/// `None`). Instead, when either of the `Option` objects is `None`, the other +/// one is necessarily returned by `fold_or`. Normal folding still occurs when +/// both values are `Some`, using `T`'s `Fold` implementation. +/// +/// This is useful when `None` in a particular context means "unspecified" +/// rather than "absent", in which case a specified value (`Some`) is chosen +/// over an unspecified one (`None`), while two specified values are folded +/// together. +pub trait AlternativeFold { + /// Attempts to fold this inner value with an outer value. However, if + /// either value is `None`, returns the other one instead of folding. + fn fold_or(self, outer: Self) -> Self; +} + +impl<T: Fold> AlternativeFold for Option<T> { + fn fold_or(self, outer: Self) -> Self { + match (self, outer) { + (Some(inner), Some(outer)) => Some(inner.fold(outer)), + // If one of values is `None`, return the other one instead of + // folding. + (inner, outer) => inner.or(outer), + } + } +} + +/// A type that accumulates depth when folded. +#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] +pub struct Depth(pub usize); + +impl Fold for Depth { + fn fold(self, outer: Self) -> Self { + Self(outer.0 + self.0) + } +} diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs new file mode 100644 index 00000000..86676fa2 --- /dev/null +++ b/crates/typst-library/src/foundations/symbol.rs @@ -0,0 +1,356 @@ +#[doc(inline)] +pub use typst_macros::symbols; + +use std::cmp::Reverse; +use std::collections::BTreeSet; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::foundations::{cast, func, scope, ty, Array, Func}; + +/// A Unicode symbol. +/// +/// Typst defines common symbols so that they can easily be written with +/// standard keyboards. The symbols are defined in modules, from which they can +/// be accessed using [field access notation]($scripting/#fields): +/// +/// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) +/// +/// Moreover, you can define custom symbols with this type's constructor +/// function. +/// +/// ```example +/// #sym.arrow.r \ +/// #sym.gt.eq.not \ +/// $gt.eq.not$ \ +/// #emoji.face.halo +/// ``` +/// +/// Many symbols have different variants, which can be selected by appending the +/// modifiers with dot notation. The order of the modifiers is not relevant. +/// Visit the documentation pages of the symbol modules and click on a symbol to +/// see its available variants. +/// +/// ```example +/// $arrow.l$ \ +/// $arrow.r$ \ +/// $arrow.t.quad$ +/// ``` +#[ty(scope, cast)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Symbol(Repr); + +/// The character of a symbol, possibly with a function. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct SymChar(char, Option<fn() -> Func>); + +/// The internal representation. +#[derive(Clone, Eq, PartialEq, Hash)] +enum Repr { + Single(SymChar), + Const(&'static [(&'static str, SymChar)]), + Multi(Arc<(List, EcoString)>), +} + +/// A collection of symbols. +#[derive(Clone, Eq, PartialEq, Hash)] +enum List { + Static(&'static [(&'static str, SymChar)]), + Runtime(Box<[(EcoString, SymChar)]>), +} + +impl Symbol { + /// Create a new symbol from a single character. + pub const fn single(c: SymChar) -> Self { + Self(Repr::Single(c)) + } + + /// Create a symbol with a static variant list. + #[track_caller] + pub const fn list(list: &'static [(&'static str, SymChar)]) -> Self { + debug_assert!(!list.is_empty()); + Self(Repr::Const(list)) + } + + /// Create a symbol with a runtime variant list. + #[track_caller] + pub fn runtime(list: Box<[(EcoString, SymChar)]>) -> Self { + debug_assert!(!list.is_empty()); + Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new())))) + } + + /// Get the symbol's char. + pub fn get(&self) -> char { + self.sym().char() + } + + /// Resolve the symbol's `SymChar`. + pub fn sym(&self) -> SymChar { + match &self.0 { + Repr::Single(c) => *c, + Repr::Const(_) => find(self.variants(), "").unwrap(), + Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(), + } + } + + /// Try to get the function associated with the symbol, if any. + pub fn func(&self) -> StrResult<Func> { + self.sym() + .func() + .ok_or_else(|| eco_format!("symbol {self} is not callable")) + } + + /// Apply a modifier to the symbol. + pub fn modified(mut self, modifier: &str) -> StrResult<Self> { + if let Repr::Const(list) = self.0 { + self.0 = Repr::Multi(Arc::new((List::Static(list), EcoString::new()))); + } + + if let Repr::Multi(arc) = &mut self.0 { + let (list, modifiers) = Arc::make_mut(arc); + if !modifiers.is_empty() { + modifiers.push('.'); + } + modifiers.push_str(modifier); + if find(list.variants(), modifiers).is_some() { + return Ok(self); + } + } + + bail!("unknown symbol modifier") + } + + /// The characters that are covered by this symbol. + pub fn variants(&self) -> impl Iterator<Item = (&str, SymChar)> { + match &self.0 { + Repr::Single(c) => Variants::Single(Some(*c).into_iter()), + Repr::Const(list) => Variants::Static(list.iter()), + Repr::Multi(arc) => arc.0.variants(), + } + } + + /// Possible modifiers. + pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ { + let mut set = BTreeSet::new(); + let modifiers = match &self.0 { + Repr::Multi(arc) => arc.1.as_str(), + _ => "", + }; + for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { + if !modifier.is_empty() && !contained(modifiers, modifier) { + set.insert(modifier); + } + } + set.into_iter() + } +} + +#[scope] +impl Symbol { + /// Create a custom symbol with modifiers. + /// + /// ```example + /// #let envelope = symbol( + /// "🖂", + /// ("stamped", "🖃"), + /// ("stamped.pen", "🖆"), + /// ("lightning", "🖄"), + /// ("fly", "🖅"), + /// ) + /// + /// #envelope + /// #envelope.stamped + /// #envelope.stamped.pen + /// #envelope.lightning + /// #envelope.fly + /// ``` + #[func(constructor)] + pub fn construct( + /// The callsite span. + span: Span, + /// The variants of the symbol. + /// + /// Can be a just a string consisting of a single character for the + /// modifierless variant or an array with two strings specifying the modifiers + /// and the symbol. Individual modifiers should be separated by dots. When + /// displaying a symbol, Typst selects the first from the variants that have + /// all attached modifiers and the minimum number of other modifiers. + #[variadic] + variants: Vec<Spanned<SymbolVariant>>, + ) -> SourceResult<Symbol> { + let mut list = Vec::new(); + if variants.is_empty() { + bail!(span, "expected at least one variant"); + } + for Spanned { v, span } in variants { + if list.iter().any(|(prev, _)| &v.0 == prev) { + bail!(span, "duplicate variant"); + } + list.push((v.0, SymChar::pure(v.1))); + } + Ok(Symbol::runtime(list.into_boxed_slice())) + } +} + +impl Display for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +impl SymChar { + /// Create a symbol character without a function. + pub const fn pure(c: char) -> Self { + Self(c, None) + } + + /// Create a symbol character with a function. + pub const fn with_func(c: char, func: fn() -> Func) -> Self { + Self(c, Some(func)) + } + + /// Get the character of the symbol. + pub const fn char(&self) -> char { + self.0 + } + + /// Get the function associated with the symbol. + pub fn func(&self) -> Option<Func> { + self.1.map(|f| f()) + } +} + +impl From<char> for SymChar { + fn from(c: char) -> Self { + SymChar(c, None) + } +} + +impl Debug for Repr { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Single(c) => Debug::fmt(c, f), + Self::Const(list) => list.fmt(f), + Self::Multi(lists) => lists.fmt(f), + } + } +} + +impl Debug for List { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Static(list) => list.fmt(f), + Self::Runtime(list) => list.fmt(f), + } + } +} + +impl crate::foundations::Repr for Symbol { + fn repr(&self) -> EcoString { + eco_format!("\"{}\"", self.get()) + } +} + +impl Serialize for Symbol { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_char(self.get()) + } +} + +impl List { + /// The characters that are covered by this list. + fn variants(&self) -> Variants<'_> { + match self { + List::Static(list) => Variants::Static(list.iter()), + List::Runtime(list) => Variants::Runtime(list.iter()), + } + } +} + +/// A value that can be cast to a symbol. +pub struct SymbolVariant(EcoString, char); + +cast! { + SymbolVariant, + c: char => Self(EcoString::new(), c), + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + +/// Iterator over variants. +enum Variants<'a> { + Single(std::option::IntoIter<SymChar>), + Static(std::slice::Iter<'static, (&'static str, SymChar)>), + Runtime(std::slice::Iter<'a, (EcoString, SymChar)>), +} + +impl<'a> Iterator for Variants<'a> { + type Item = (&'a str, SymChar); + + fn next(&mut self) -> Option<Self::Item> { + match self { + Self::Single(iter) => Some(("", iter.next()?)), + Self::Static(list) => list.next().copied(), + Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), + } + } +} + +/// Find the best symbol from the list. +fn find<'a>( + variants: impl Iterator<Item = (&'a str, SymChar)>, + modifiers: &str, +) -> Option<SymChar> { + let mut best = None; + let mut best_score = None; + + // Find the best table entry with this name. + 'outer: for candidate in variants { + for modifier in parts(modifiers) { + if !contained(candidate.0, modifier) { + continue 'outer; + } + } + + let mut matching = 0; + let mut total = 0; + for modifier in parts(candidate.0) { + if contained(modifiers, modifier) { + matching += 1; + } + total += 1; + } + + let score = (matching, Reverse(total)); + if best_score.map_or(true, |b| score > b) { + best = Some(candidate.1); + best_score = Some(score); + } + } + + best +} + +/// Split a modifier list into its parts. +fn parts(modifiers: &str) -> impl Iterator<Item = &str> { + modifiers.split('.').filter(|s| !s.is_empty()) +} + +/// Whether the modifier string contains the modifier `m`. +fn contained(modifiers: &str, m: &str) -> bool { + parts(modifiers).any(|part| part == m) +} diff --git a/crates/typst-library/src/foundations/sys.rs b/crates/typst-library/src/foundations/sys.rs new file mode 100644 index 00000000..7c128104 --- /dev/null +++ b/crates/typst-library/src/foundations/sys.rs @@ -0,0 +1,18 @@ +//! System-related things. + +use crate::foundations::{Dict, Module, Scope, Version}; + +/// A module with system-related things. +pub fn module(inputs: Dict) -> Module { + let mut scope = Scope::deduplicating(); + scope.define( + "version", + Version::from_iter([ + env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>().unwrap(), + env!("CARGO_PKG_VERSION_MINOR").parse::<u32>().unwrap(), + env!("CARGO_PKG_VERSION_PATCH").parse::<u32>().unwrap(), + ]), + ); + scope.define("inputs", inputs); + Module::new("sys", scope) +} diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs new file mode 100644 index 00000000..70845dd2 --- /dev/null +++ b/crates/typst-library/src/foundations/ty.rs @@ -0,0 +1,223 @@ +#[doc(inline)] +pub use typst_macros::{scope, ty}; + +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter}; + +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use typst_utils::Static; + +use crate::diag::StrResult; +use crate::foundations::{ + cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, +}; + +/// Describes a kind of value. +/// +/// To style your document, you need to work with values of different kinds: +/// Lengths specifying the size of your elements, colors for your text and +/// shapes, and more. Typst categorizes these into clearly defined _types_ and +/// tells you where it expects which type of value. +/// +/// Apart from basic types for numeric values and [typical]($int) +/// [types]($float) [known]($str) [from]($array) [programming]($dictionary) +/// languages, Typst provides a special type for [_content._]($content) A value +/// of this type can hold anything that you can enter into your document: Text, +/// elements like headings and shapes, and style information. +/// +/// # Example +/// ```example +/// #let x = 10 +/// #if type(x) == int [ +/// #x is an integer! +/// ] else [ +/// #x is another value... +/// ] +/// +/// An image is of type +/// #type(image("glacier.jpg")). +/// ``` +/// +/// The type of `10` is `int`. Now, what is the type of `int` or even `type`? +/// ```example +/// #type(int) \ +/// #type(type) +/// ``` +/// +/// # Compatibility +/// In Typst 0.7 and lower, the `type` function returned a string instead of a +/// type. Compatibility with the old way will remain for a while to give package +/// authors time to upgrade, but it will be removed at some point. +/// +/// - Checks like `{int == "integer"}` evaluate to `{true}` +/// - Adding/joining a type and string will yield a string +/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}` +/// if the dictionary has a string key matching the type's name +#[ty(scope, cast)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Type(Static<NativeTypeData>); + +impl Type { + /// Get the type for `T`. + pub fn of<T: NativeType>() -> Self { + T::ty() + } + + /// The type's short name, how it is used in code (e.g. `str`). + pub fn short_name(&self) -> &'static str { + self.0.name + } + + /// The type's long name, for use in diagnostics (e.g. `string`). + pub fn long_name(&self) -> &'static str { + self.0.long_name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the type (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs + } + + /// Search keywords for the type. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// This type's constructor function. + pub fn constructor(&self) -> StrResult<Func> { + self.0 + .constructor + .as_ref() + .map(|lazy| Func::from(*lazy)) + .ok_or_else(|| eco_format!("type {self} does not have a constructor")) + } + + /// The type's associated scope that holds sub-definitions. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Get a field from this type's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + self.scope() + .get(field) + .ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) + } +} + +// Type compatibility. +impl Type { + /// The type's backward-compatible name. + pub fn compat_name(&self) -> &str { + self.long_name() + } +} + +#[scope] +impl Type { + /// Determines a value's type. + /// + /// ```example + /// #type(12) \ + /// #type(14.7) \ + /// #type("hello") \ + /// #type(<glacier>) \ + /// #type([Hi]) \ + /// #type(x => x + 1) \ + /// #type(type) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value whose type's to determine. + value: Value, + ) -> Type { + value.ty() + } +} + +impl Debug for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Type({})", self.long_name()) + } +} + +impl Repr for Type { + fn repr(&self) -> EcoString { + if *self == Type::of::<AutoValue>() { + "type(auto)" + } else if *self == Type::of::<NoneValue>() { + "type(none)" + } else { + self.long_name() + } + .into() + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.long_name()) + } +} + +impl Ord for Type { + fn cmp(&self, other: &Self) -> Ordering { + self.long_name().cmp(other.long_name()) + } +} + +impl PartialOrd for Type { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +/// A Typst type that is defined by a native Rust type. +pub trait NativeType { + /// The type's name. + /// + /// In contrast to `data()`, this is usable in const contexts. + const NAME: &'static str; + + /// Get the type for the native Rust type. + fn ty() -> Type { + Type::from(Self::data()) + } + + // Get the type data for the native Rust type. + fn data() -> &'static NativeTypeData; +} + +/// Defines a native type. +#[derive(Debug)] +pub struct NativeTypeData { + /// The type's normal name (e.g. `str`), as exposed to Typst. + pub name: &'static str, + pub long_name: &'static str, + /// The function's title case name (e.g. `String`). + pub title: &'static str, + /// The documentation for this type as a string. + pub docs: &'static str, + /// A list of alternate search terms for this type. + pub keywords: &'static [&'static str], + /// The constructor for this type. + pub constructor: Lazy<Option<&'static NativeFuncData>>, + pub scope: Lazy<Scope>, +} + +impl From<&'static NativeTypeData> for Type { + fn from(data: &'static NativeTypeData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeTypeData, + self => Type::from(self).into_value(), +} diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs new file mode 100644 index 00000000..fbfa5a0e --- /dev/null +++ b/crates/typst-library/src/foundations/value.rs @@ -0,0 +1,761 @@ +use std::any::{Any, TypeId}; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer}; +use serde::de::{Error, MapAccess, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::{ast, Span}; +use typst_utils::ArcExt; + +use crate::diag::{HintedStrResult, HintedString, StrResult}; +use crate::foundations::{ + fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, + Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, + NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, + Styles, Symbol, Type, Version, +}; +use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; +use crate::text::{RawContent, RawElem, TextElem}; +use crate::visualize::{Color, Gradient, Pattern}; + +/// A computational value. +#[derive(Default, Clone)] +pub enum Value { + /// The value that indicates the absence of a meaningful value. + #[default] + None, + /// A value that indicates some smart default behaviour. + Auto, + /// A boolean: `true, false`. + Bool(bool), + /// An integer: `120`. + Int(i64), + /// A floating-point number: `1.2`, `10e-4`. + Float(f64), + /// A length: `12pt`, `3cm`, `1.5em`, `1em - 2pt`. + Length(Length), + /// An angle: `1.5rad`, `90deg`. + Angle(Angle), + /// A ratio: `50%`. + Ratio(Ratio), + /// A relative length, combination of a ratio and a length: `20% + 5cm`. + Relative(Rel<Length>), + /// A fraction: `1fr`. + Fraction(Fr), + /// A color value: `#f79143ff`. + Color(Color), + /// A gradient value: `gradient.linear(...)`. + Gradient(Gradient), + /// A pattern fill: `pattern(...)`. + Pattern(Pattern), + /// A symbol: `arrow.l`. + Symbol(Symbol), + /// A version. + Version(Version), + /// A string: `"string"`. + Str(Str), + /// Raw bytes. + Bytes(Bytes), + /// A label: `<intro>`. + Label(Label), + /// A datetime + Datetime(Datetime), + /// A decimal value: `decimal("123.4500")` + Decimal(Decimal), + /// A duration + Duration(Duration), + /// A content value: `[*Hi* there]`. + Content(Content), + // Content styles. + Styles(Styles), + /// An array of values: `(1, "hi", 12cm)`. + Array(Array), + /// A dictionary value: `(a: 1, b: "hi")`. + Dict(Dict), + /// An executable function. + Func(Func), + /// Captured arguments to a function. + Args(Args), + /// A type. + Type(Type), + /// A module. + Module(Module), + /// A WebAssembly plugin. + Plugin(Plugin), + /// A dynamic value. + Dyn(Dynamic), +} + +impl Value { + /// Create a new dynamic value. + pub fn dynamic<T>(any: T) -> Self + where + T: Debug + Repr + NativeType + PartialEq + Hash + Sync + Send + 'static, + { + Self::Dyn(Dynamic::new(any)) + } + + /// Create a numeric value from a number with a unit. + pub fn numeric(pair: (f64, ast::Unit)) -> Self { + let (v, unit) = pair; + match unit { + ast::Unit::Pt => Abs::pt(v).into_value(), + ast::Unit::Mm => Abs::mm(v).into_value(), + ast::Unit::Cm => Abs::cm(v).into_value(), + ast::Unit::In => Abs::inches(v).into_value(), + ast::Unit::Rad => Angle::rad(v).into_value(), + ast::Unit::Deg => Angle::deg(v).into_value(), + ast::Unit::Em => Em::new(v).into_value(), + ast::Unit::Fr => Fr::new(v).into_value(), + ast::Unit::Percent => Ratio::new(v / 100.0).into_value(), + } + } + + /// The type of this value. + pub fn ty(&self) -> Type { + match self { + Self::None => Type::of::<NoneValue>(), + Self::Auto => Type::of::<AutoValue>(), + Self::Bool(_) => Type::of::<bool>(), + Self::Int(_) => Type::of::<i64>(), + Self::Float(_) => Type::of::<f64>(), + Self::Length(_) => Type::of::<Length>(), + Self::Angle(_) => Type::of::<Angle>(), + Self::Ratio(_) => Type::of::<Ratio>(), + Self::Relative(_) => Type::of::<Rel<Length>>(), + Self::Fraction(_) => Type::of::<Fr>(), + Self::Color(_) => Type::of::<Color>(), + Self::Gradient(_) => Type::of::<Gradient>(), + Self::Pattern(_) => Type::of::<Pattern>(), + Self::Symbol(_) => Type::of::<Symbol>(), + Self::Version(_) => Type::of::<Version>(), + Self::Str(_) => Type::of::<Str>(), + Self::Bytes(_) => Type::of::<Bytes>(), + Self::Label(_) => Type::of::<Label>(), + Self::Datetime(_) => Type::of::<Datetime>(), + Self::Decimal(_) => Type::of::<Decimal>(), + Self::Duration(_) => Type::of::<Duration>(), + Self::Content(_) => Type::of::<Content>(), + Self::Styles(_) => Type::of::<Styles>(), + Self::Array(_) => Type::of::<Array>(), + Self::Dict(_) => Type::of::<Dict>(), + Self::Func(_) => Type::of::<Func>(), + Self::Args(_) => Type::of::<Args>(), + Self::Type(_) => Type::of::<Type>(), + Self::Module(_) => Type::of::<Module>(), + Self::Plugin(_) => Type::of::<Module>(), + Self::Dyn(v) => v.ty(), + } + } + + /// Try to cast the value into a specific type. + pub fn cast<T: FromValue>(self) -> HintedStrResult<T> { + T::from_value(self) + } + + /// Try to access a field on the value. + pub fn field(&self, field: &str) -> StrResult<Value> { + match self { + Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), + Self::Version(version) => version.component(field).map(Self::Int), + Self::Dict(dict) => dict.get(field).cloned(), + Self::Content(content) => content.field_by_name(field), + Self::Type(ty) => ty.field(field).cloned(), + Self::Func(func) => func.field(field).cloned(), + Self::Module(module) => module.field(field).cloned(), + _ => fields::field(self, field), + } + } + + /// The associated scope, if this is a function, type, or module. + pub fn scope(&self) -> Option<&Scope> { + match self { + Self::Func(func) => func.scope(), + Self::Type(ty) => Some(ty.scope()), + Self::Module(module) => Some(module.scope()), + _ => None, + } + } + + /// The name, if this is a function, type, or module. + pub fn name(&self) -> Option<&str> { + match self { + Self::Func(func) => func.name(), + Self::Type(ty) => Some(ty.short_name()), + Self::Module(module) => Some(module.name()), + _ => None, + } + } + + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { + match self { + Self::Func(func) => func.docs(), + Self::Type(ty) => Some(ty.docs()), + _ => None, + } + } + + /// Return the display representation of the value. + pub fn display(self) -> Content { + match self { + Self::None => Content::empty(), + Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)), + Self::Float(v) => TextElem::packed(repr::display_float(v)), + Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), + Self::Str(v) => TextElem::packed(v), + Self::Version(v) => TextElem::packed(eco_format!("{v}")), + Self::Symbol(v) => TextElem::packed(v.get()), + Self::Content(v) => v, + Self::Module(module) => module.content(), + _ => RawElem::new(RawContent::Text(self.repr())) + .with_lang(Some("typc".into())) + .with_block(false) + .pack(), + } + } + + /// Attach a span to the value, if possible. + pub fn spanned(self, span: Span) -> Self { + match self { + Value::Content(v) => Value::Content(v.spanned(span)), + Value::Func(v) => Value::Func(v.spanned(span)), + v => v, + } + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::None => Debug::fmt(&NoneValue, f), + Self::Auto => Debug::fmt(&AutoValue, f), + Self::Bool(v) => Debug::fmt(v, f), + Self::Int(v) => Debug::fmt(v, f), + Self::Float(v) => Debug::fmt(v, f), + Self::Length(v) => Debug::fmt(v, f), + Self::Angle(v) => Debug::fmt(v, f), + Self::Ratio(v) => Debug::fmt(v, f), + Self::Relative(v) => Debug::fmt(v, f), + Self::Fraction(v) => Debug::fmt(v, f), + Self::Color(v) => Debug::fmt(v, f), + Self::Gradient(v) => Debug::fmt(v, f), + Self::Pattern(v) => Debug::fmt(v, f), + Self::Symbol(v) => Debug::fmt(v, f), + Self::Version(v) => Debug::fmt(v, f), + Self::Str(v) => Debug::fmt(v, f), + Self::Bytes(v) => Debug::fmt(v, f), + Self::Label(v) => Debug::fmt(v, f), + Self::Datetime(v) => Debug::fmt(v, f), + Self::Decimal(v) => Debug::fmt(v, f), + Self::Duration(v) => Debug::fmt(v, f), + Self::Content(v) => Debug::fmt(v, f), + Self::Styles(v) => Debug::fmt(v, f), + Self::Array(v) => Debug::fmt(v, f), + Self::Dict(v) => Debug::fmt(v, f), + Self::Func(v) => Debug::fmt(v, f), + Self::Args(v) => Debug::fmt(v, f), + Self::Type(v) => Debug::fmt(v, f), + Self::Module(v) => Debug::fmt(v, f), + Self::Plugin(v) => Debug::fmt(v, f), + Self::Dyn(v) => Debug::fmt(v, f), + } + } +} + +impl Repr for Value { + fn repr(&self) -> EcoString { + match self { + Self::None => NoneValue.repr(), + Self::Auto => AutoValue.repr(), + Self::Bool(v) => v.repr(), + Self::Int(v) => v.repr(), + Self::Float(v) => v.repr(), + Self::Length(v) => v.repr(), + Self::Angle(v) => v.repr(), + Self::Ratio(v) => v.repr(), + Self::Relative(v) => v.repr(), + Self::Fraction(v) => v.repr(), + Self::Color(v) => v.repr(), + Self::Gradient(v) => v.repr(), + Self::Pattern(v) => v.repr(), + Self::Symbol(v) => v.repr(), + Self::Version(v) => v.repr(), + Self::Str(v) => v.repr(), + Self::Bytes(v) => v.repr(), + Self::Label(v) => v.repr(), + Self::Datetime(v) => v.repr(), + Self::Decimal(v) => v.repr(), + Self::Duration(v) => v.repr(), + Self::Content(v) => v.repr(), + Self::Styles(v) => v.repr(), + Self::Array(v) => v.repr(), + Self::Dict(v) => v.repr(), + Self::Func(v) => v.repr(), + Self::Args(v) => v.repr(), + Self::Type(v) => v.repr(), + Self::Module(v) => v.repr(), + Self::Plugin(v) => v.repr(), + Self::Dyn(v) => v.repr(), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + ops::equal(self, other) + } +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + ops::compare(self, other).ok() + } +} + +impl Hash for Value { + fn hash<H: Hasher>(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::None => {} + Self::Auto => {} + Self::Bool(v) => v.hash(state), + Self::Int(v) => v.hash(state), + Self::Float(v) => v.to_bits().hash(state), + Self::Length(v) => v.hash(state), + Self::Angle(v) => v.hash(state), + Self::Ratio(v) => v.hash(state), + Self::Relative(v) => v.hash(state), + Self::Fraction(v) => v.hash(state), + Self::Color(v) => v.hash(state), + Self::Gradient(v) => v.hash(state), + Self::Pattern(v) => v.hash(state), + Self::Symbol(v) => v.hash(state), + Self::Version(v) => v.hash(state), + Self::Str(v) => v.hash(state), + Self::Bytes(v) => v.hash(state), + Self::Label(v) => v.hash(state), + Self::Content(v) => v.hash(state), + Self::Styles(v) => v.hash(state), + Self::Datetime(v) => v.hash(state), + Self::Decimal(v) => v.hash(state), + Self::Duration(v) => v.hash(state), + Self::Array(v) => v.hash(state), + Self::Dict(v) => v.hash(state), + Self::Func(v) => v.hash(state), + Self::Args(v) => v.hash(state), + Self::Type(v) => v.hash(state), + Self::Module(v) => v.hash(state), + Self::Plugin(v) => v.hash(state), + Self::Dyn(v) => v.hash(state), + } + } +} + +impl Serialize for Value { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match self { + Self::None => NoneValue.serialize(serializer), + Self::Bool(v) => v.serialize(serializer), + Self::Int(v) => v.serialize(serializer), + Self::Float(v) => v.serialize(serializer), + Self::Str(v) => v.serialize(serializer), + Self::Bytes(v) => v.serialize(serializer), + Self::Symbol(v) => v.serialize(serializer), + Self::Content(v) => v.serialize(serializer), + Self::Array(v) => v.serialize(serializer), + Self::Dict(v) => v.serialize(serializer), + + // Fall back to repr() for other things. + other => serializer.serialize_str(&other.repr()), + } + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(ValueVisitor) + } +} + +/// Visitor for value deserialization. +struct ValueVisitor; + +impl<'de> Visitor<'de> for ValueVisitor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a typst value") + } + + fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i8<E: Error>(self, v: i8) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i16<E: Error>(self, v: i16) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i32<E: Error>(self, v: i32) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u8<E: Error>(self, v: u8) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u16<E: Error>(self, v: u16) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u32<E: Error>(self, v: u32) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_f32<E: Error>(self, v: f32) -> Result<Self::Value, E> { + Ok((v as f64).into_value()) + } + + fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_char<E: Error>(self, v: char) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_borrowed_str<E: Error>(self, v: &'de str) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_none<E: Error>(self) -> Result<Self::Value, E> { + Ok(Value::None) + } + + fn visit_some<D: Deserializer<'de>>( + self, + deserializer: D, + ) -> Result<Self::Value, D::Error> { + Value::deserialize(deserializer) + } + + fn visit_unit<E: Error>(self) -> Result<Self::Value, E> { + Ok(Value::None) + } + + fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> { + Ok(Array::deserialize(SeqAccessDeserializer::new(seq))?.into_value()) + } + + fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> { + let dict = Dict::deserialize(MapAccessDeserializer::new(map))?; + Ok(match Datetime::from_toml_dict(&dict) { + None => dict.into_value(), + Some(datetime) => datetime.into_value(), + }) + } +} + +/// A value that is not part of the built-in enum. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Dynamic(Arc<dyn Bounds>); + +impl Dynamic { + /// Create a new instance from any value that satisfies the required bounds. + pub fn new<T>(any: T) -> Self + where + T: Debug + Repr + NativeType + PartialEq + Hash + Sync + Send + 'static, + { + Self(Arc::new(any)) + } + + /// Whether the wrapped type is `T`. + pub fn is<T: 'static>(&self) -> bool { + (*self.0).as_any().is::<T>() + } + + /// Try to downcast to a reference to a specific type. + pub fn downcast<T: 'static>(&self) -> Option<&T> { + (*self.0).as_any().downcast_ref() + } + + /// The name of the stored value's type. + pub fn ty(&self) -> Type { + self.0.dyn_ty() + } +} + +impl Debug for Dynamic { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Repr for Dynamic { + fn repr(&self) -> EcoString { + self.0.repr() + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +trait Bounds: Debug + Repr + Sync + Send + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &Dynamic) -> bool; + fn dyn_ty(&self) -> Type; + fn dyn_hash(&self, state: &mut dyn Hasher); +} + +impl<T> Bounds for T +where + T: Debug + Repr + NativeType + PartialEq + Hash + Sync + Send + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &Dynamic) -> bool { + let Some(other) = other.downcast::<Self>() else { return false }; + self == other + } + + fn dyn_ty(&self) -> Type { + Type::of::<T>() + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + // Also hash the TypeId since values with different types but + // equal data should be different. + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// Implements traits for primitives (Value enum variants). +macro_rules! primitive { + ( + $ty:ty: $name:literal, $variant:ident + $(, $other:ident$(($binding:ident))? => $out:expr)* + ) => { + impl Reflect for $ty { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::$variant(_) + $(| primitive!(@$other $(($binding))?))*) + } + } + + impl IntoValue for $ty { + fn into_value(self) -> Value { + Value::$variant(self) + } + } + + impl FromValue for $ty { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::$variant(v) => Ok(v), + $(Value::$other$(($binding))? => Ok($out),)* + v => Err(<Self as Reflect>::error(&v)), + } + } + } + }; + + (@$other:ident($binding:ident)) => { Value::$other(_) }; + (@$other:ident) => { Value::$other }; +} + +primitive! { bool: "boolean", Bool } +primitive! { i64: "integer", Int } +primitive! { f64: "float", Float, Int(v) => v as f64 } +primitive! { Length: "length", Length } +primitive! { Angle: "angle", Angle } +primitive! { Ratio: "ratio", Ratio } +primitive! { Rel<Length>: "relative length", + Relative, + Length(v) => v.into(), + Ratio(v) => v.into() +} +primitive! { Fr: "fraction", Fraction } +primitive! { Color: "color", Color } +primitive! { Gradient: "gradient", Gradient } +primitive! { Pattern: "pattern", Pattern } +primitive! { Symbol: "symbol", Symbol } +primitive! { Version: "version", Version } +primitive! { + Str: "string", + Str, + Symbol(symbol) => symbol.get().into() +} +primitive! { Bytes: "bytes", Bytes } +primitive! { Label: "label", Label } +primitive! { Datetime: "datetime", Datetime } +primitive! { Decimal: "decimal", Decimal } +primitive! { Duration: "duration", Duration } +primitive! { Content: "content", + Content, + None => Content::empty(), + Symbol(v) => TextElem::packed(v.get()), + Str(v) => TextElem::packed(v) +} +primitive! { Styles: "styles", Styles } +primitive! { Array: "array", Array } +primitive! { Dict: "dictionary", Dict } +primitive! { + Func: "function", + Func, + Type(ty) => ty.constructor()?.clone(), + Symbol(symbol) => symbol.func()? +} +primitive! { Args: "arguments", Args } +primitive! { Type: "type", Type } +primitive! { Module: "module", Module } +primitive! { Plugin: "plugin", Plugin } + +impl<T: Reflect> Reflect for Arc<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } + + fn error(found: &Value) -> HintedString { + T::error(found) + } +} + +impl<T: Clone + IntoValue> IntoValue for Arc<T> { + fn into_value(self) -> Value { + Arc::take(self).into_value() + } +} + +impl<T: FromValue> FromValue for Arc<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + v if T::castable(&v) => Ok(Arc::new(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} + +impl<T: Clone + Resolve> Resolve for Arc<T> { + type Output = Arc<T::Output>; + + fn resolve(self, styles: super::StyleChain) -> Self::Output { + Arc::new(Arc::take(self).resolve(styles)) + } +} + +impl<T: Clone + Fold> Fold for Arc<T> { + fn fold(self, outer: Self) -> Self { + Arc::new(Arc::take(self).fold(Arc::take(outer))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::foundations::{array, dict}; + + #[track_caller] + fn test(value: impl IntoValue, exp: &str) { + assert_eq!(value.into_value().repr(), exp); + } + + #[test] + fn test_value_debug() { + // Primitives. + test(Value::None, "none"); + test(Value::Auto, "auto"); + test(Value::None.ty(), "type(none)"); + test(Value::Auto.ty(), "type(auto)"); + test(false, "false"); + test(12i64, "12"); + test(3.24, "3.24"); + test(Abs::pt(5.5), "5.5pt"); + test(Angle::deg(90.0), "90deg"); + test(Ratio::one() / 2.0, "50%"); + test(Ratio::new(0.3) + Length::from(Abs::cm(2.0)), "30% + 56.69pt"); + test(Fr::one() * 7.55, "7.55fr"); + + // Collections. + test("hello", r#""hello""#); + test("\n", r#""\n""#); + test("\\", r#""\\""#); + test("\"", r#""\"""#); + test(array![], "()"); + test(array![Value::None], "(none,)"); + test(array![1, 2], "(1, 2)"); + test(dict![], "(:)"); + test(dict!["one" => 1], "(one: 1)"); + test(dict!["two" => false, "one" => 1], "(two: false, one: 1)"); + } +} diff --git a/crates/typst-library/src/foundations/version.rs b/crates/typst-library/src/foundations/version.rs new file mode 100644 index 00000000..62c02917 --- /dev/null +++ b/crates/typst-library/src/foundations/version.rs @@ -0,0 +1,202 @@ +use std::cmp::Ordering; +use std::fmt::{self, Display, Formatter, Write}; +use std::hash::Hash; +use std::iter::repeat; + +use ecow::{eco_format, EcoString, EcoVec}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, func, repr, scope, ty, Repr}; + +/// A version with an arbitrary number of components. +/// +/// The first three components have names that can be used as fields: `major`, +/// `minor`, `patch`. All following components do not have names. +/// +/// The list of components is semantically extended by an infinite list of +/// zeros. This means that, for example, `0.8` is the same as `0.8.0`. As a +/// special case, the empty version (that has no components at all) is the same +/// as `0`, `0.0`, `0.0.0`, and so on. +/// +/// The current version of the Typst compiler is available as `sys.version`. +/// +/// You can convert a version to an array of explicitly given components using +/// the [`array`] constructor. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Version(EcoVec<u32>); + +impl Version { + /// The names for the first components of a version. + pub const COMPONENTS: [&'static str; 3] = ["major", "minor", "patch"]; + + /// Create a new (empty) version. + pub fn new() -> Self { + Self::default() + } + + /// Get a named component of a version. + /// + /// Always non-negative. Returns `0` if the version isn't specified to the + /// necessary length. + pub fn component(&self, name: &str) -> StrResult<i64> { + self.0 + .iter() + .zip(Self::COMPONENTS) + .find_map(|(&i, s)| (s == name).then_some(i as i64)) + .ok_or_else(|| "unknown version component".into()) + } + + /// Push a component to the end of this version. + pub fn push(&mut self, component: u32) { + self.0.push(component); + } + + /// The values of the version + pub fn values(&self) -> &[u32] { + &self.0 + } +} + +#[scope] +impl Version { + /// Creates a new version. + /// + /// It can have any number of components (even zero). + /// + /// ```example + /// #version() \ + /// #version(1) \ + /// #version(1, 2, 3, 4) \ + /// #version((1, 2, 3, 4)) \ + /// #version((1, 2), 3) + /// ``` + #[func(constructor)] + pub fn construct( + /// The components of the version (array arguments are flattened) + #[variadic] + components: Vec<VersionComponents>, + ) -> Version { + let mut version = Version::new(); + for c in components { + match c { + VersionComponents::Single(v) => version.push(v), + VersionComponents::Multiple(values) => { + for v in values { + version.push(v); + } + } + } + } + version + } + + /// Retrieves a component of a version. + /// + /// The returned integer is always non-negative. Returns `0` if the version + /// isn't specified to the necessary length. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the component. If negative, indexes + /// from the back of the explicitly given components. + index: i64, + ) -> StrResult<i64> { + let mut index = index; + if index < 0 { + match (self.0.len() as i64).checked_add(index) { + Some(pos_index) if pos_index >= 0 => index = pos_index, + _ => bail!( + "component index out of bounds (index: {index}, len: {})", + self.0.len() + ), + } + } + Ok(usize::try_from(index) + .ok() + .and_then(|i| self.0.get(i).copied()) + .unwrap_or_default() as i64) + } +} + +impl FromIterator<u32> for Version { + fn from_iter<T: IntoIterator<Item = u32>>(iter: T) -> Self { + Self(EcoVec::from_iter(iter)) + } +} + +impl IntoIterator for Version { + type Item = u32; + type IntoIter = ecow::vec::IntoIter<u32>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + let max_len = self.0.len().max(other.0.len()); + let tail = repeat(&0); + + let self_iter = self.0.iter().chain(tail.clone()); + let other_iter = other.0.iter().chain(tail); + + for (l, r) in self_iter.zip(other_iter).take(max_len) { + match l.cmp(r) { + Ordering::Equal => (), + ord => return ord, + } + } + + Ordering::Equal + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Eq for Version {} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Equal) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut first = true; + for &v in &self.0 { + if !first { + f.write_char('.')?; + } + write!(f, "{v}")?; + first = false; + } + Ok(()) + } +} + +impl Repr for Version { + fn repr(&self) -> EcoString { + let parts: Vec<_> = self.0.iter().map(|v| eco_format!("{v}")).collect(); + eco_format!("version{}", &repr::pretty_array_like(&parts, false)) + } +} + +/// One or multiple version components. +pub enum VersionComponents { + Single(u32), + Multiple(Vec<u32>), +} + +cast! { + VersionComponents, + v: u32 => Self::Single(v), + v: Vec<u32> => Self::Multiple(v) +} |
