summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/foundations
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/foundations')
-rw-r--r--crates/typst-library/src/foundations/args.rs453
-rw-r--r--crates/typst-library/src/foundations/array.rs1133
-rw-r--r--crates/typst-library/src/foundations/auto.rs267
-rw-r--r--crates/typst-library/src/foundations/bool.rs26
-rw-r--r--crates/typst-library/src/foundations/bytes.rs262
-rw-r--r--crates/typst-library/src/foundations/calc.rs1211
-rw-r--r--crates/typst-library/src/foundations/cast.rs499
-rw-r--r--crates/typst-library/src/foundations/content.rs1007
-rw-r--r--crates/typst-library/src/foundations/context.rs85
-rw-r--r--crates/typst-library/src/foundations/datetime.rs580
-rw-r--r--crates/typst-library/src/foundations/decimal.rs499
-rw-r--r--crates/typst-library/src/foundations/dict.rs395
-rw-r--r--crates/typst-library/src/foundations/duration.rs215
-rw-r--r--crates/typst-library/src/foundations/element.rs333
-rw-r--r--crates/typst-library/src/foundations/fields.rs91
-rw-r--r--crates/typst-library/src/foundations/float.rs190
-rw-r--r--crates/typst-library/src/foundations/func.rs541
-rw-r--r--crates/typst-library/src/foundations/int.rs478
-rw-r--r--crates/typst-library/src/foundations/label.rs93
-rw-r--r--crates/typst-library/src/foundations/mod.rs302
-rw-r--r--crates/typst-library/src/foundations/module.rs142
-rw-r--r--crates/typst-library/src/foundations/none.rs114
-rw-r--r--crates/typst-library/src/foundations/ops.rs583
-rw-r--r--crates/typst-library/src/foundations/plugin.rs365
-rw-r--r--crates/typst-library/src/foundations/repr.rs235
-rw-r--r--crates/typst-library/src/foundations/scope.rs416
-rw-r--r--crates/typst-library/src/foundations/selector.rs449
-rw-r--r--crates/typst-library/src/foundations/str.rs969
-rw-r--r--crates/typst-library/src/foundations/styles.rs1034
-rw-r--r--crates/typst-library/src/foundations/symbol.rs356
-rw-r--r--crates/typst-library/src/foundations/sys.rs18
-rw-r--r--crates/typst-library/src/foundations/ty.rs223
-rw-r--r--crates/typst-library/src/foundations/value.rs761
-rw-r--r--crates/typst-library/src/foundations/version.rs202
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(&regex::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(&regex.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)
+}