summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/foundations/version.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/foundations/version.rs')
-rw-r--r--crates/typst-library/src/foundations/version.rs202
1 files changed, 202 insertions, 0 deletions
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)
+}