Skip to main content

typst_library/foundations/
version.rs

1use std::cmp::Ordering;
2use std::fmt::{self, Display, Formatter, Write};
3use std::hash::Hash;
4use std::iter::repeat;
5
6use ecow::{EcoString, EcoVec, eco_format};
7
8use crate::diag::{StrResult, bail};
9use crate::foundations::{Repr, cast, func, repr, scope, ty};
10
11/// A version with an arbitrary number of components.
12///
13/// The first three components have names that can be used as fields: `major`,
14/// `minor`, `patch`. All following components do not have names.
15///
16/// The list of components is semantically extended by an infinite list of
17/// zeros. This means that, for example, `0.8` is the same as `0.8.0`. As a
18/// special case, the empty version (that has no components at all) is the same
19/// as `0`, `0.0`, `0.0.0`, and so on.
20///
21/// The current version of the Typst compiler is available as `sys.version`.
22///
23/// You can convert a version to an array of explicitly given components using
24/// the @array constructor.
25#[ty(scope, cast)]
26#[derive(Debug, Default, Clone, Hash)]
27pub struct Version(EcoVec<u32>);
28
29impl Version {
30    /// The names for the first components of a version.
31    pub const COMPONENTS: [&'static str; 3] = ["major", "minor", "patch"];
32
33    /// Create a new (empty) version.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Get a named component of a version.
39    ///
40    /// Always non-negative. Returns `0` if the version isn't specified to the
41    /// necessary length.
42    pub fn component(&self, name: &str) -> StrResult<i64> {
43        self.0
44            .iter()
45            .zip(Self::COMPONENTS)
46            .find_map(|(&i, s)| (s == name).then_some(i as i64))
47            .ok_or_else(|| "unknown version component".into())
48    }
49
50    /// Push a component to the end of this version.
51    pub fn push(&mut self, component: u32) {
52        self.0.push(component);
53    }
54
55    /// The values of the version
56    pub fn values(&self) -> &[u32] {
57        &self.0
58    }
59}
60
61#[scope]
62impl Version {
63    /// Creates a new version.
64    ///
65    /// It can have any number of components (even zero).
66    ///
67    /// #example(
68    ///   title: "Constructing versions",
69    ///   ```
70    ///   #version() \
71    ///   #version(1) \
72    ///   #version(1, 2, 3, 4) \
73    ///   #version((1, 2, 3, 4)) \
74    ///   #version((1, 2), 3)
75    ///   ```
76    /// )
77    ///
78    /// As a practical use case, this allows comparing the current version
79    /// (@version[`{sys.version}`]) to a specific one.
80    ///
81    /// #example(
82    ///   title: "Comparing with the current version",
83    ///   ```
84    ///   Current version: #sys.version \
85    ///   #(sys.version >= version(0, 14, 0)) \
86    ///   #(version(3, 2, 0) > version(4, 1, 0))
87    ///   ```
88    /// )
89    #[func(constructor)]
90    pub fn construct(
91        /// The components of the version (array arguments are flattened)
92        #[variadic]
93        components: Vec<VersionComponents>,
94    ) -> Version {
95        let mut version = Version::new();
96        for c in components {
97            match c {
98                VersionComponents::Single(v) => version.push(v),
99                VersionComponents::Multiple(values) => {
100                    for v in values {
101                        version.push(v);
102                    }
103                }
104            }
105        }
106        version
107    }
108
109    /// Retrieves a component of a version.
110    ///
111    /// The returned integer is always non-negative. Returns `0` if the version
112    /// isn't specified to the necessary length.
113    #[func]
114    pub fn at(
115        &self,
116        /// The index at which to retrieve the component. If negative, indexes
117        /// from the back of the explicitly given components.
118        index: i64,
119    ) -> StrResult<i64> {
120        let mut index = index;
121        if index < 0 {
122            match (self.0.len() as i64).checked_add(index) {
123                Some(pos_index) if pos_index >= 0 => index = pos_index,
124                _ => bail!(
125                    "component index out of bounds (index: {index}, len: {})",
126                    self.0.len(),
127                ),
128            }
129        }
130        Ok(usize::try_from(index)
131            .ok()
132            .and_then(|i| self.0.get(i).copied())
133            .unwrap_or_default() as i64)
134    }
135}
136
137impl FromIterator<u32> for Version {
138    fn from_iter<T: IntoIterator<Item = u32>>(iter: T) -> Self {
139        Self(EcoVec::from_iter(iter))
140    }
141}
142
143impl IntoIterator for Version {
144    type Item = u32;
145    type IntoIter = ecow::vec::IntoIter<u32>;
146
147    fn into_iter(self) -> Self::IntoIter {
148        self.0.into_iter()
149    }
150}
151
152impl Ord for Version {
153    fn cmp(&self, other: &Self) -> Ordering {
154        let max_len = self.0.len().max(other.0.len());
155        let tail = repeat(&0);
156
157        let self_iter = self.0.iter().chain(tail.clone());
158        let other_iter = other.0.iter().chain(tail);
159
160        for (l, r) in self_iter.zip(other_iter).take(max_len) {
161            match l.cmp(r) {
162                Ordering::Equal => (),
163                ord => return ord,
164            }
165        }
166
167        Ordering::Equal
168    }
169}
170
171impl PartialOrd for Version {
172    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
173        Some(self.cmp(other))
174    }
175}
176
177impl Eq for Version {}
178
179impl PartialEq for Version {
180    fn eq(&self, other: &Self) -> bool {
181        matches!(self.cmp(other), Ordering::Equal)
182    }
183}
184
185impl Display for Version {
186    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
187        let mut first = true;
188        for &v in &self.0 {
189            if !first {
190                f.write_char('.')?;
191            }
192            write!(f, "{v}")?;
193            first = false;
194        }
195        Ok(())
196    }
197}
198
199impl Repr for Version {
200    fn repr(&self) -> EcoString {
201        let parts: Vec<_> = self.0.iter().map(|v| eco_format!("{v}")).collect();
202        eco_format!("version{}", &repr::pretty_array_like(&parts, false))
203    }
204}
205
206/// One or multiple version components.
207pub enum VersionComponents {
208    Single(u32),
209    Multiple(Vec<u32>),
210}
211
212cast! {
213    VersionComponents,
214    v: u32 => Self::Single(v),
215    v: Vec<u32> => Self::Multiple(v)
216}