diff options
| -rw-r--r-- | crates/typst-cli/src/main.rs | 1 | ||||
| -rw-r--r-- | crates/typst-library/src/compute/foundations.rs | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/compute/mod.rs | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/compute/sys.rs | 24 | ||||
| -rw-r--r-- | crates/typst/src/eval/array.rs | 3 | ||||
| -rw-r--r-- | crates/typst/src/eval/fields.rs | 9 | ||||
| -rw-r--r-- | crates/typst/src/eval/mod.rs | 2 | ||||
| -rw-r--r-- | crates/typst/src/eval/ops.rs | 2 | ||||
| -rw-r--r-- | crates/typst/src/eval/str.rs | 3 | ||||
| -rw-r--r-- | crates/typst/src/eval/value.rs | 10 | ||||
| -rw-r--r-- | crates/typst/src/eval/version.rs | 199 | ||||
| -rw-r--r-- | tests/typ/compiler/bytes.typ | 2 | ||||
| -rw-r--r-- | tests/typ/compute/construct.typ | 2 | ||||
| -rw-r--r-- | tests/typ/compute/version.typ | 47 |
14 files changed, 301 insertions, 8 deletions
diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index c5cfd514..bab1a07b 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -11,7 +11,6 @@ mod watch; mod world; use std::cell::Cell; -use std::env; use std::io::{self, IsTerminal, Write}; use std::process::ExitCode; diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs index dad05717..250a9f57 100644 --- a/crates/typst-library/src/compute/foundations.rs +++ b/crates/typst-library/src/compute/foundations.rs @@ -1,5 +1,5 @@ use typst::eval::{ - Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, + Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, Version, }; use crate::prelude::*; @@ -22,6 +22,7 @@ pub(super) fn define(global: &mut Scope) { global.define_type::<Regex>(); global.define_type::<Datetime>(); global.define_type::<Duration>(); + global.define_type::<Version>(); global.define_type::<Plugin>(); global.define_func::<repr>(); global.define_func::<panic>(); diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs index 9e897653..f1af24c5 100644 --- a/crates/typst-library/src/compute/mod.rs +++ b/crates/typst-library/src/compute/mod.rs @@ -1,6 +1,7 @@ //! Computational functions. pub mod calc; +pub mod sys; mod data; mod foundations; @@ -15,4 +16,5 @@ pub(super) fn define(global: &mut Scope) { self::foundations::define(global); self::data::define(global); self::calc::define(global); + self::sys::define(global); } diff --git a/crates/typst-library/src/compute/sys.rs b/crates/typst-library/src/compute/sys.rs new file mode 100644 index 00000000..6404e625 --- /dev/null +++ b/crates/typst-library/src/compute/sys.rs @@ -0,0 +1,24 @@ +//! System-related things. + +use typst::eval::{Module, Scope, Version}; + +/// Hook up all calculation definitions. +pub(super) fn define(global: &mut Scope) { + global.category("sys"); + global.define_module(module()); +} + +/// A module with system-related things. +fn module() -> Module { + let mut scope = Scope::deduplicating(); + scope.category("sys"); + 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(), + ]), + ); + Module::new("sys", scope) +} diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs index 8e9e5f27..3da7c0b0 100644 --- a/crates/typst/src/eval/array.rs +++ b/crates/typst/src/eval/array.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use super::{ cast, func, ops, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, - Reflect, Value, Vm, + Reflect, Value, Version, Vm, }; use crate::diag::{At, SourceResult, StrResult}; use crate::eval::ops::{add, mul}; @@ -804,6 +804,7 @@ cast! { ToArray, v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()), v: Array => Self(v), + v: Version => Self(v.values().iter().map(|&v| Value::Int(v as i64)).collect()) } impl Debug for Array { diff --git a/crates/typst/src/eval/fields.rs b/crates/typst/src/eval/fields.rs index 7d164497..37e490e6 100644 --- a/crates/typst/src/eval/fields.rs +++ b/crates/typst/src/eval/fields.rs @@ -1,6 +1,7 @@ use ecow::{eco_format, EcoString}; use crate::diag::StrResult; +use crate::eval::Version; use crate::geom::{Align, Length, Rel, Stroke}; use super::{IntoValue, Type, Value}; @@ -16,6 +17,10 @@ pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> { // 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(), @@ -69,7 +74,9 @@ fn missing_field(ty: Type, field: &str) -> EcoString { /// List the available fields for a type. pub fn fields_on(ty: Type) -> &'static [&'static str] { - if ty == Type::of::<Length>() { + if ty == Type::of::<Version>() { + &Version::COMPONENTS + } else if ty == Type::of::<Length>() { &["em", "abs"] } else if ty == Type::of::<Rel>() { &["ratio", "length"] diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 424e15ee..f0d1be20 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -31,6 +31,7 @@ mod scope; mod symbol; mod tracer; mod ty; +mod version; #[doc(hidden)] pub use { @@ -65,6 +66,7 @@ pub use self::symbol::{symbols, Symbol}; pub use self::tracer::Tracer; pub use self::ty::{scope, ty, NativeType, NativeTypeData, Type}; pub use self::value::{Dynamic, Value}; +pub use self::version::Version; use std::collections::HashSet; use std::mem; diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs index 23a32416..9280b6c6 100644 --- a/crates/typst/src/eval/ops.rs +++ b/crates/typst/src/eval/ops.rs @@ -366,6 +366,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (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, @@ -408,6 +409,7 @@ pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> { (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. diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs index 6684389b..3a8d4730 100644 --- a/crates/typst/src/eval/str.rs +++ b/crates/typst/src/eval/str.rs @@ -9,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{ cast, dict, func, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Type, Value, - Vm, + Version, Vm, }; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::geom::Align; @@ -605,6 +605,7 @@ cast! { ToStr, v: i64 => Self::Int(v), v: f64 => 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")? diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index 69edcdec..c53f2df4 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/eval/value.rs @@ -14,7 +14,7 @@ use typst::eval::Duration; use super::{ fields, format_str, ops, Args, Array, AutoValue, Bytes, CastInfo, Content, Dict, FromValue, Func, IntoValue, Module, NativeType, NoneValue, Plugin, Reflect, Scope, - Str, Symbol, Type, + Str, Symbol, Type, Version, }; use crate::diag::StrResult; use crate::eval::Datetime; @@ -50,6 +50,8 @@ pub enum Value { Color(Color), /// A symbol: `arrow.l`. Symbol(Symbol), + /// A version. + Version(Version), /// A string: `"string"`. Str(Str), /// Raw bytes. @@ -122,6 +124,7 @@ impl Value { Self::Fraction(_) => Type::of::<Fr>(), Self::Color(_) => Type::of::<Color>(), 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>(), @@ -149,6 +152,7 @@ impl 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.get(field), Self::Type(ty) => ty.field(field).cloned(), @@ -199,6 +203,7 @@ impl Value { Self::Int(v) => item!(text)(eco_format!("{v}")), Self::Float(v) => item!(text)(eco_format!("{v}")), Self::Str(v) => item!(text)(v.into()), + Self::Version(v) => item!(text)(eco_format!("{v}")), Self::Symbol(v) => item!(text)(v.get().into()), Self::Content(v) => v, Self::Module(module) => module.content(), @@ -231,6 +236,7 @@ impl Debug for Value { Self::Fraction(v) => Debug::fmt(v, f), Self::Color(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), @@ -278,6 +284,7 @@ impl Hash for Value { Self::Fraction(v) => v.hash(state), Self::Color(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), @@ -582,6 +589,7 @@ primitive! { Rel<Length>: "relative length", primitive! { Fr: "fraction", Fraction } primitive! { Color: "color", Color } primitive! { Symbol: "symbol", Symbol } +primitive! { Version: "version", Version } primitive! { Str: "string", Str, diff --git a/crates/typst/src/eval/version.rs b/crates/typst/src/eval/version.rs new file mode 100644 index 00000000..6ba0dff4 --- /dev/null +++ b/crates/typst/src/eval/version.rs @@ -0,0 +1,199 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::hash::Hash; +use std::iter::repeat; + +use ecow::{eco_format, EcoVec}; + +use super::{cast, func, scope, ty}; +use crate::diag::{bail, error, StrResult}; +use crate::util::pretty_array_like; + +/// A version, with any number of components. +/// +/// 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 first three components have names: `major`, `minor`, `patch`. All +/// components after that do not have names. +#[ty(scope)] +#[derive(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(|| error!("unknown version component")) + } + + /// 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 + } + + /// Get a component of a version. + /// + /// 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) -> std::fmt::Result { + let mut first = true; + for &v in &self.0 { + if !first { + f.write_char('.')?; + } + write!(f, "{v}")?; + first = false; + } + Ok(()) + } +} + +impl Debug for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("version")?; + let parts: Vec<_> = self.0.iter().map(|v| eco_format!("{v}")).collect(); + f.write_str(&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) +} diff --git a/tests/typ/compiler/bytes.typ b/tests/typ/compiler/bytes.typ index fa724858..9a26e826 100644 --- a/tests/typ/compiler/bytes.typ +++ b/tests/typ/compiler/bytes.typ @@ -28,5 +28,5 @@ #bytes((a: 1)) --- -// Error: 8-15 expected bytes or array, found string +// Error: 8-15 expected bytes, array, or version, found string #array("hello") diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index d3cea0b4..6980c922 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -168,7 +168,7 @@ #test(str(10 / 3).len() > 10, true) --- -// Error: 6-8 expected integer, float, bytes, label, type, or string, found content +// Error: 6-8 expected integer, float, version, bytes, label, type, or string, found content #str([]) --- diff --git a/tests/typ/compute/version.typ b/tests/typ/compute/version.typ new file mode 100644 index 00000000..e33eeb6f --- /dev/null +++ b/tests/typ/compute/version.typ @@ -0,0 +1,47 @@ +// Test versions. +// Ref: false + +--- +// Test version constructor. + +// Empty. +#version() + +// Plain. +#version(1, 2) + +// Single Array argument. +#version((1, 2)) + +// Mixed arguments. +#version(1, (2, 3), 4, (5, 6), 7) + +--- +// Test equality of different-length versions +#test(version(), version(0)) +#test(version(0), version(0, 0)) +#test(version(1, 2), version(1, 2, 0, 0, 0, 0)) +--- +// Test `version.at`. + +// Non-negative index in bounds +#test(version(1, 2).at(1), 2) + +// Non-negative index out of bounds +#test(version(1, 2).at(4), 0) + +// Negative index in bounds +#test(version(1, 2).at(-2), 1) + +// Error: 2-22 component index out of bounds (index: -3, len: 2) +#version(1, 2).at(-3) + +--- +// Test version fields. +#test(version(1, 2, 3).major, 1) +#test(version(1, 2, 3).minor, 2) +#test(version(1, 2, 3).patch, 3) + +--- +// Test the type of `sys.version` +#test(type(sys.version), version) |
