summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/foundations/args.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/foundations/args.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/foundations/args.rs')
-rw-r--r--crates/typst-library/src/foundations/args.rs453
1 files changed, 453 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(),
+ }
+ )
+}