1use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6use typst::syntax::VirtualPath;
7use typst::syntax::package::PackageSpec;
8
9pub const MANIFEST_PATH: &str = "typst-pack.toml";
11
12pub const FORMAT_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case", deny_unknown_fields)]
18pub struct Manifest {
19 pub format_version: u32,
21 pub project: ProjectManifest,
23 #[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
25 pub packages: PackagesManifest,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub fonts: Vec<FontManifest>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub metadata: Option<Metadata>,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case", deny_unknown_fields)]
37pub struct ProjectManifest {
38 pub entrypoint: String,
40}
41
42#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "kebab-case", deny_unknown_fields)]
45pub struct PackagesManifest {
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub vendored: Vec<String>,
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub external: Vec<String>,
54}
55
56impl PackagesManifest {
57 fn is_empty(&self) -> bool {
58 self.vendored.is_empty() && self.external.is_empty()
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "kebab-case", deny_unknown_fields)]
65pub struct FontManifest {
66 pub path: String,
68 #[serde(default, skip_serializing_if = "is_zero")]
70 pub index: u32,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub families: Vec<String>,
74}
75
76fn is_zero(index: &u32) -> bool {
77 *index == 0
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "kebab-case", deny_unknown_fields)]
83pub struct Metadata {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub name: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub description: Option<String>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub authors: Vec<String>,
90}
91
92#[derive(Debug, thiserror::Error)]
94pub enum ManifestError {
95 #[error("failed to parse manifest: {0}")]
96 Parse(#[from] toml::de::Error),
97 #[error("unsupported pack format version {0} (this reader supports up to {FORMAT_VERSION})")]
98 UnsupportedVersion(u32),
99 #[error("invalid entrypoint path `{path}`: {message}")]
100 InvalidEntrypoint { path: String, message: String },
101 #[error("invalid package spec `{spec}`: {message}")]
102 InvalidPackageSpec { spec: String, message: String },
103 #[error("invalid font path `{path}`: {message}")]
104 InvalidFontPath { path: String, message: String },
105}
106
107impl Manifest {
108 pub fn from_toml(text: &str) -> Result<Self, ManifestError> {
110 let manifest: Manifest = toml::from_str(text)?;
111 manifest.validate()?;
112 Ok(manifest)
113 }
114
115 pub fn to_toml(&self) -> String {
117 toml::to_string_pretty(self).expect("manifest is always serializable")
118 }
119
120 pub fn validate(&self) -> Result<(), ManifestError> {
122 if self.format_version > FORMAT_VERSION {
123 return Err(ManifestError::UnsupportedVersion(self.format_version));
124 }
125 self.entrypoint()?;
126 for spec in self.packages.vendored.iter().chain(&self.packages.external) {
127 parse_spec(spec)?;
128 }
129 for font in &self.fonts {
130 if let Err(err) = VirtualPath::new(&font.path) {
131 return Err(ManifestError::InvalidFontPath {
132 path: font.path.clone(),
133 message: err.to_string(),
134 });
135 }
136 }
137 Ok(())
138 }
139
140 pub fn entrypoint(&self) -> Result<VirtualPath, ManifestError> {
142 VirtualPath::new(&self.project.entrypoint).map_err(|err| ManifestError::InvalidEntrypoint {
143 path: self.project.entrypoint.clone(),
144 message: err.to_string(),
145 })
146 }
147
148 pub fn vendored_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
150 self.packages
151 .vendored
152 .iter()
153 .map(|spec| parse_spec(spec))
154 .collect()
155 }
156
157 pub fn external_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
159 self.packages
160 .external
161 .iter()
162 .map(|spec| parse_spec(spec))
163 .collect()
164 }
165}
166
167fn parse_spec(spec: &str) -> Result<PackageSpec, ManifestError> {
168 PackageSpec::from_str(spec).map_err(|err| ManifestError::InvalidPackageSpec {
169 spec: spec.to_owned(),
170 message: err.to_string(),
171 })
172}