use std::str::FromStr;
use serde::{Deserialize, Serialize};
use typst::syntax::VirtualPath;
use typst::syntax::package::PackageSpec;
pub const MANIFEST_PATH: &str = "typst-pack.toml";
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Manifest {
pub format_version: u32,
pub project: ProjectManifest,
#[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
pub packages: PackagesManifest,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fonts: Vec<FontManifest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ProjectManifest {
pub entrypoint: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PackagesManifest {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub vendored: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub external: Vec<String>,
}
impl PackagesManifest {
fn is_empty(&self) -> bool {
self.vendored.is_empty() && self.external.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct FontManifest {
pub path: String,
#[serde(default, skip_serializing_if = "is_zero")]
pub index: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub families: Vec<String>,
}
fn is_zero(index: &u32) -> bool {
*index == 0
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Metadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ManifestError {
#[error("failed to parse manifest: {0}")]
Parse(#[from] toml::de::Error),
#[error("unsupported pack format version {0} (this reader supports up to {FORMAT_VERSION})")]
UnsupportedVersion(u32),
#[error("invalid entrypoint path `{path}`: {message}")]
InvalidEntrypoint { path: String, message: String },
#[error("invalid package spec `{spec}`: {message}")]
InvalidPackageSpec { spec: String, message: String },
#[error("invalid font path `{path}`: {message}")]
InvalidFontPath { path: String, message: String },
}
impl Manifest {
pub fn from_toml(text: &str) -> Result<Self, ManifestError> {
let manifest: Manifest = toml::from_str(text)?;
manifest.validate()?;
Ok(manifest)
}
pub fn to_toml(&self) -> String {
toml::to_string_pretty(self).expect("manifest is always serializable")
}
pub fn validate(&self) -> Result<(), ManifestError> {
if self.format_version > FORMAT_VERSION {
return Err(ManifestError::UnsupportedVersion(self.format_version));
}
self.entrypoint()?;
for spec in self.packages.vendored.iter().chain(&self.packages.external) {
parse_spec(spec)?;
}
for font in &self.fonts {
if let Err(err) = VirtualPath::new(&font.path) {
return Err(ManifestError::InvalidFontPath {
path: font.path.clone(),
message: err.to_string(),
});
}
}
Ok(())
}
pub fn entrypoint(&self) -> Result<VirtualPath, ManifestError> {
VirtualPath::new(&self.project.entrypoint).map_err(|err| ManifestError::InvalidEntrypoint {
path: self.project.entrypoint.clone(),
message: err.to_string(),
})
}
pub fn vendored_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
self.packages
.vendored
.iter()
.map(|spec| parse_spec(spec))
.collect()
}
pub fn external_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
self.packages
.external
.iter()
.map(|spec| parse_spec(spec))
.collect()
}
}
fn parse_spec(spec: &str) -> Result<PackageSpec, ManifestError> {
PackageSpec::from_str(spec).map_err(|err| ManifestError::InvalidPackageSpec {
spec: spec.to_owned(),
message: err.to_string(),
})
}