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