From a558fd232b379e527eac2d5196eb9367b7d28032 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 6 Mar 2024 12:33:35 +0100 Subject: Add `typst init` command (#3544) --- crates/typst-syntax/src/package.rs | 267 +++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 crates/typst-syntax/src/package.rs (limited to 'crates/typst-syntax/src/package.rs') diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs new file mode 100644 index 00000000..138b39ca --- /dev/null +++ b/crates/typst-syntax/src/package.rs @@ -0,0 +1,267 @@ +//! Package manifest parsing. + +use std::fmt::{self, Debug, Display, Formatter}; +use std::str::FromStr; + +use ecow::{eco_format, EcoString}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use unscanny::Scanner; + +use crate::is_ident; + +/// A parsed package manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageManifest { + /// Details about the package itself. + pub package: PackageInfo, + /// Details about the template, if the package is one. + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, +} + +/// The `[template]` key in the manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct TemplateInfo { + /// The path of the starting point within the package. + pub path: EcoString, + /// The path of the entrypoint relative to the starting point's `path`. + pub entrypoint: EcoString, +} + +/// The `[package]` key in the manifest. +/// +/// More fields are specified, but they are not relevant to the compiler. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageInfo { + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: PackageVersion, + /// The path of the entrypoint into the package. + pub entrypoint: EcoString, + /// The minimum required compiler version for the package. + pub compiler: Option, +} + +impl PackageManifest { + /// Ensure that this manifest is indeed for the specified package. + pub fn validate(&self, spec: &PackageSpec) -> Result<(), EcoString> { + if self.package.name != spec.name { + return Err(eco_format!( + "package manifest contains mismatched name `{}`", + self.package.name + )); + } + + if self.package.version != spec.version { + return Err(eco_format!( + "package manifest contains mismatched version {}", + self.package.version + )); + } + + if let Some(required) = self.package.compiler { + let current = PackageVersion::compiler(); + if current < required { + return Err(eco_format!( + "package requires typst {required} or newer \ + (current version is {current})" + )); + } + } + + Ok(()) + } +} + +/// Identifies a package. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct PackageSpec { + /// The namespace the package lives in. + pub namespace: EcoString, + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: PackageVersion, +} + +impl FromStr for PackageSpec { + type Err = EcoString; + + fn from_str(s: &str) -> Result { + let mut s = unscanny::Scanner::new(s); + let namespace = parse_namespace(&mut s)?.into(); + let name = parse_name(&mut s)?.into(); + let version = parse_version(&mut s)?; + Ok(Self { namespace, name, version }) + } +} + +impl Debug for PackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for PackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "@{}/{}:{}", self.namespace, self.name, self.version) + } +} + +/// Identifies a package, but not a specific version of it. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct VersionlessPackageSpec { + /// The namespace the package lives in. + pub namespace: EcoString, + /// The name of the package within its namespace. + pub name: EcoString, +} + +impl VersionlessPackageSpec { + /// Fill in the `version` to get a complete [`PackageSpec`]. + pub fn at(self, version: PackageVersion) -> PackageSpec { + PackageSpec { + namespace: self.namespace, + name: self.name, + version, + } + } +} + +impl FromStr for VersionlessPackageSpec { + type Err = EcoString; + + fn from_str(s: &str) -> Result { + let mut s = unscanny::Scanner::new(s); + let namespace = parse_namespace(&mut s)?.into(); + let name = parse_name(&mut s)?.into(); + if !s.done() { + Err("unexpected version in versionless package specification")?; + } + Ok(Self { namespace, name }) + } +} + +impl Debug for VersionlessPackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for VersionlessPackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "@{}/{}", self.namespace, self.name) + } +} + +fn parse_namespace<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> { + if !s.eat_if('@') { + Err("package specification must start with '@'")?; + } + + let namespace = s.eat_until('/'); + if namespace.is_empty() { + Err("package specification is missing namespace")?; + } else if !is_ident(namespace) { + Err(eco_format!("`{namespace}` is not a valid package namespace"))?; + } + + Ok(namespace) +} + +fn parse_name<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> { + s.eat_if('/'); + + let name = s.eat_until(':'); + if name.is_empty() { + Err("package specification is missing name")?; + } else if !is_ident(name) { + Err(eco_format!("`{name}` is not a valid package name"))?; + } + + Ok(name) +} + +fn parse_version(s: &mut Scanner) -> Result { + s.eat_if(':'); + + let version = s.after(); + if version.is_empty() { + Err("package specification is missing version")?; + } + + version.parse() +} + +/// A package's version. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct PackageVersion { + /// The package's major version. + pub major: u32, + /// The package's minor version. + pub minor: u32, + /// The package's patch version. + pub patch: u32, +} + +impl PackageVersion { + /// The current compiler version. + pub fn compiler() -> Self { + Self { + major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), + minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), + patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), + } + } +} + +impl FromStr for PackageVersion { + type Err = EcoString; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('.'); + let mut next = |kind| { + let part = parts + .next() + .filter(|s| !s.is_empty()) + .ok_or_else(|| eco_format!("version number is missing {kind} version"))?; + part.parse::() + .map_err(|_| eco_format!("`{part}` is not a valid {kind} version")) + }; + + let major = next("major")?; + let minor = next("minor")?; + let patch = next("patch")?; + if let Some(rest) = parts.next() { + Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?; + } + + Ok(Self { major, minor, patch }) + } +} + +impl Debug for PackageVersion { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for PackageVersion { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Serialize for PackageVersion { + fn serialize(&self, s: S) -> Result { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for PackageVersion { + fn deserialize>(d: D) -> Result { + let string = EcoString::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} -- cgit v1.2.3