typst_syntax/
package.rs

1//! Package manifest parsing.
2
3use std::collections::BTreeMap;
4use std::fmt::{self, Debug, Display, Formatter};
5use std::str::FromStr;
6
7use ecow::{EcoString, eco_format};
8use serde::de::IgnoredAny;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use unscanny::Scanner;
11
12use crate::is_ident;
13
14/// A type alias for a map of key-value pairs used to collect unknown fields
15/// where values are completely discarded.
16pub type UnknownFields = BTreeMap<EcoString, IgnoredAny>;
17
18/// A parsed package manifest.
19///
20/// The `unknown_fields` contains fields which were found but not expected.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct PackageManifest {
23    /// Details about the package itself.
24    pub package: PackageInfo,
25    /// Details about the template, if the package is one.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub template: Option<TemplateInfo>,
28    /// The tools section for third-party configuration.
29    #[serde(default)]
30    pub tool: ToolInfo,
31    /// All parsed but unknown fields, this can be used for validation.
32    #[serde(flatten, skip_serializing)]
33    pub unknown_fields: UnknownFields,
34}
35
36/// The `[tool]` key in the manifest. This field can be used to retrieve
37/// 3rd-party tool configuration.
38///
39/// # Examples
40/// ```
41/// # use serde::{Deserialize, Serialize};
42/// # use ecow::EcoString;
43/// # use typst_syntax::package::PackageManifest;
44/// #[derive(Debug, PartialEq, Serialize, Deserialize)]
45/// struct MyTool {
46///     key: EcoString,
47/// }
48///
49/// let mut manifest: PackageManifest = toml::from_str(r#"
50///     [package]
51///     name = "package"
52///     version = "0.1.0"
53///     entrypoint = "src/lib.typ"
54///
55///     [tool.my-tool]
56///     key = "value"
57/// "#)?;
58///
59/// let my_tool = manifest
60///     .tool
61///     .sections
62///     .remove("my-tool")
63///     .ok_or("tool.my-tool section missing")?;
64/// let my_tool = MyTool::deserialize(my_tool)?;
65///
66/// assert_eq!(my_tool, MyTool { key: "value".into() });
67/// # Ok::<_, Box<dyn std::error::Error>>(())
68/// ```
69#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
70pub struct ToolInfo {
71    /// Any fields parsed in the tool section.
72    #[serde(flatten)]
73    pub sections: BTreeMap<EcoString, toml::Table>,
74}
75
76/// The `[template]` key in the manifest.
77///
78/// The `unknown_fields` contains fields which were found but not expected.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct TemplateInfo {
81    /// The directory within the package that contains the files that should be
82    /// copied into the user's new project directory.
83    pub path: EcoString,
84    /// A path relative to the template's path that points to the file serving
85    /// as the compilation target.
86    pub entrypoint: EcoString,
87    /// A path relative to the package's root that points to a PNG or lossless
88    /// WebP thumbnail for the template.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub thumbnail: Option<EcoString>,
91    /// All parsed but unknown fields, this can be used for validation.
92    #[serde(flatten, skip_serializing)]
93    pub unknown_fields: UnknownFields,
94}
95
96/// The `[package]` key in the manifest.
97///
98/// The `unknown_fields` contains fields which were found but not expected.
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct PackageInfo {
101    /// The name of the package within its namespace.
102    pub name: EcoString,
103    /// The package's version.
104    pub version: PackageVersion,
105    /// The path of the entrypoint into the package.
106    pub entrypoint: EcoString,
107    /// A list of the package's authors.
108    #[serde(default, skip_serializing_if = "Vec::is_empty")]
109    pub authors: Vec<EcoString>,
110    ///  The package's license.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub license: Option<EcoString>,
113    /// A short description of the package.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub description: Option<EcoString>,
116    /// A link to the package's web presence.
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub homepage: Option<EcoString>,
119    /// A link to the repository where this package is developed.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub repository: Option<EcoString>,
122    /// An array of search keywords for the package.
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub keywords: Vec<EcoString>,
125    /// An array with up to three of the predefined categories to help users
126    /// discover the package.
127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
128    pub categories: Vec<EcoString>,
129    /// An array of disciplines defining the target audience for which the
130    /// package is useful.
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub disciplines: Vec<EcoString>,
133    /// The minimum required compiler version for the package.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub compiler: Option<VersionBound>,
136    /// An array of globs specifying files that should not be part of the
137    /// published bundle.
138    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139    pub exclude: Vec<EcoString>,
140    /// All parsed but unknown fields, this can be used for validation.
141    #[serde(flatten, skip_serializing)]
142    pub unknown_fields: UnknownFields,
143}
144
145impl PackageManifest {
146    /// Create a new package manifest with the given package info.
147    pub fn new(package: PackageInfo) -> Self {
148        PackageManifest {
149            package,
150            template: None,
151            tool: ToolInfo::default(),
152            unknown_fields: UnknownFields::new(),
153        }
154    }
155
156    /// Ensure that this manifest is indeed for the specified package.
157    pub fn validate(&self, spec: &PackageSpec) -> Result<(), EcoString> {
158        if self.package.name != spec.name {
159            return Err(eco_format!(
160                "package manifest contains mismatched name `{}`",
161                self.package.name
162            ));
163        }
164
165        if self.package.version != spec.version {
166            return Err(eco_format!(
167                "package manifest contains mismatched version {}",
168                self.package.version
169            ));
170        }
171
172        if let Some(required) = self.package.compiler {
173            let current = PackageVersion::compiler();
174            if !current.matches_ge(&required) {
175                return Err(eco_format!(
176                    "package requires Typst {required} or newer \
177                     (current version is {current})"
178                ));
179            }
180        }
181
182        Ok(())
183    }
184}
185
186impl TemplateInfo {
187    /// Create a new template info with only required fields.
188    pub fn new(path: impl Into<EcoString>, entrypoint: impl Into<EcoString>) -> Self {
189        TemplateInfo {
190            path: path.into(),
191            entrypoint: entrypoint.into(),
192            thumbnail: None,
193            unknown_fields: UnknownFields::new(),
194        }
195    }
196}
197
198impl PackageInfo {
199    /// Create a new package info with only required fields.
200    pub fn new(
201        name: impl Into<EcoString>,
202        version: PackageVersion,
203        entrypoint: impl Into<EcoString>,
204    ) -> Self {
205        PackageInfo {
206            name: name.into(),
207            version,
208            entrypoint: entrypoint.into(),
209            authors: vec![],
210            categories: vec![],
211            compiler: None,
212            description: None,
213            disciplines: vec![],
214            exclude: vec![],
215            homepage: None,
216            keywords: vec![],
217            license: None,
218            repository: None,
219            unknown_fields: BTreeMap::new(),
220        }
221    }
222}
223
224/// Identifies a package.
225#[derive(Clone, Eq, PartialEq, Hash)]
226pub struct PackageSpec {
227    /// The namespace the package lives in.
228    pub namespace: EcoString,
229    /// The name of the package within its namespace.
230    pub name: EcoString,
231    /// The package's version.
232    pub version: PackageVersion,
233}
234
235impl PackageSpec {
236    pub fn versionless(&self) -> VersionlessPackageSpec {
237        VersionlessPackageSpec {
238            namespace: self.namespace.clone(),
239            name: self.name.clone(),
240        }
241    }
242}
243
244impl FromStr for PackageSpec {
245    type Err = EcoString;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        let mut s = unscanny::Scanner::new(s);
249        let namespace = parse_namespace(&mut s)?.into();
250        let name = parse_name(&mut s)?.into();
251        let version = parse_version(&mut s)?;
252        Ok(Self { namespace, name, version })
253    }
254}
255
256impl Debug for PackageSpec {
257    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
258        Display::fmt(self, f)
259    }
260}
261
262impl Display for PackageSpec {
263    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
264        write!(f, "@{}/{}:{}", self.namespace, self.name, self.version)
265    }
266}
267
268/// Identifies a package, but not a specific version of it.
269#[derive(Clone, Eq, PartialEq, Hash)]
270pub struct VersionlessPackageSpec {
271    /// The namespace the package lives in.
272    pub namespace: EcoString,
273    /// The name of the package within its namespace.
274    pub name: EcoString,
275}
276
277impl VersionlessPackageSpec {
278    /// Fill in the `version` to get a complete [`PackageSpec`].
279    pub fn at(self, version: PackageVersion) -> PackageSpec {
280        PackageSpec {
281            namespace: self.namespace,
282            name: self.name,
283            version,
284        }
285    }
286}
287
288impl FromStr for VersionlessPackageSpec {
289    type Err = EcoString;
290
291    fn from_str(s: &str) -> Result<Self, Self::Err> {
292        let mut s = unscanny::Scanner::new(s);
293        let namespace = parse_namespace(&mut s)?.into();
294        let name = parse_name(&mut s)?.into();
295        if !s.done() {
296            Err("unexpected version in versionless package specification")?;
297        }
298        Ok(Self { namespace, name })
299    }
300}
301
302impl Debug for VersionlessPackageSpec {
303    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
304        Display::fmt(self, f)
305    }
306}
307
308impl Display for VersionlessPackageSpec {
309    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
310        write!(f, "@{}/{}", self.namespace, self.name)
311    }
312}
313
314fn parse_namespace<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
315    if !s.eat_if('@') {
316        Err("package specification must start with '@'")?;
317    }
318
319    let namespace = s.eat_until('/');
320    if namespace.is_empty() {
321        Err("package specification is missing namespace")?;
322    } else if !is_ident(namespace) {
323        Err(eco_format!("`{namespace}` is not a valid package namespace"))?;
324    }
325
326    Ok(namespace)
327}
328
329fn parse_name<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
330    s.eat_if('/');
331
332    let name = s.eat_until(':');
333    if name.is_empty() {
334        Err("package specification is missing name")?;
335    } else if !is_ident(name) {
336        Err(eco_format!("`{name}` is not a valid package name"))?;
337    }
338
339    Ok(name)
340}
341
342fn parse_version(s: &mut Scanner) -> Result<PackageVersion, EcoString> {
343    s.eat_if(':');
344
345    let version = s.after();
346    if version.is_empty() {
347        Err("package specification is missing version")?;
348    }
349
350    version.parse()
351}
352
353/// A package's version.
354#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
355pub struct PackageVersion {
356    /// The package's major version.
357    pub major: u32,
358    /// The package's minor version.
359    pub minor: u32,
360    /// The package's patch version.
361    pub patch: u32,
362}
363
364impl PackageVersion {
365    /// The current compiler version.
366    pub fn compiler() -> Self {
367        Self {
368            major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
369            minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
370            patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
371        }
372    }
373
374    /// Performs an `==` match with the given version bound. Version elements
375    /// missing in the bound are ignored.
376    pub fn matches_eq(&self, bound: &VersionBound) -> bool {
377        self.major == bound.major
378            && bound.minor.is_none_or(|minor| self.minor == minor)
379            && bound.patch.is_none_or(|patch| self.patch == patch)
380    }
381
382    /// Performs a `>` match with the given version bound. The match only
383    /// succeeds if some version element in the bound is actually greater than
384    /// that of the version.
385    pub fn matches_gt(&self, bound: &VersionBound) -> bool {
386        if self.major != bound.major {
387            return self.major > bound.major;
388        }
389        let Some(minor) = bound.minor else { return false };
390        if self.minor != minor {
391            return self.minor > minor;
392        }
393        let Some(patch) = bound.patch else { return false };
394        if self.patch != patch {
395            return self.patch > patch;
396        }
397        false
398    }
399
400    /// Performs a `<` match with the given version bound. The match only
401    /// succeeds if some version element in the bound is actually less than that
402    /// of the version.
403    pub fn matches_lt(&self, bound: &VersionBound) -> bool {
404        if self.major != bound.major {
405            return self.major < bound.major;
406        }
407        let Some(minor) = bound.minor else { return false };
408        if self.minor != minor {
409            return self.minor < minor;
410        }
411        let Some(patch) = bound.patch else { return false };
412        if self.patch != patch {
413            return self.patch < patch;
414        }
415        false
416    }
417
418    /// Performs a `>=` match with the given versions. The match succeeds when
419    /// either a `==` or `>` match does.
420    pub fn matches_ge(&self, bound: &VersionBound) -> bool {
421        self.matches_eq(bound) || self.matches_gt(bound)
422    }
423
424    /// Performs a `<=` match with the given versions. The match succeeds when
425    /// either a `==` or `<` match does.
426    pub fn matches_le(&self, bound: &VersionBound) -> bool {
427        self.matches_eq(bound) || self.matches_lt(bound)
428    }
429}
430
431impl FromStr for PackageVersion {
432    type Err = EcoString;
433
434    fn from_str(s: &str) -> Result<Self, Self::Err> {
435        let mut parts = s.split('.');
436        let mut next = |kind| {
437            let part = parts
438                .next()
439                .filter(|s| !s.is_empty())
440                .ok_or_else(|| eco_format!("version number is missing {kind} version"))?;
441            part.parse::<u32>()
442                .map_err(|_| eco_format!("`{part}` is not a valid {kind} version"))
443        };
444
445        let major = next("major")?;
446        let minor = next("minor")?;
447        let patch = next("patch")?;
448        if let Some(rest) = parts.next() {
449            Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?;
450        }
451
452        Ok(Self { major, minor, patch })
453    }
454}
455
456impl Debug for PackageVersion {
457    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
458        Display::fmt(self, f)
459    }
460}
461
462impl Display for PackageVersion {
463    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
464        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
465    }
466}
467
468impl Serialize for PackageVersion {
469    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
470        s.collect_str(self)
471    }
472}
473
474impl<'de> Deserialize<'de> for PackageVersion {
475    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
476        let string = EcoString::deserialize(d)?;
477        string.parse().map_err(serde::de::Error::custom)
478    }
479}
480
481/// A version bound for compatibility specification.
482#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
483pub struct VersionBound {
484    /// The bounds's major version.
485    pub major: u32,
486    /// The bounds's minor version.
487    pub minor: Option<u32>,
488    /// The bounds's patch version. Can only be present if minor is too.
489    pub patch: Option<u32>,
490}
491
492impl FromStr for VersionBound {
493    type Err = EcoString;
494
495    fn from_str(s: &str) -> Result<Self, Self::Err> {
496        let mut parts = s.split('.');
497        let mut next = |kind| {
498            if let Some(part) = parts.next() {
499                part.parse::<u32>().map(Some).map_err(|_| {
500                    eco_format!("`{part}` is not a valid {kind} version bound")
501                })
502            } else {
503                Ok(None)
504            }
505        };
506
507        let major = next("major")?
508            .ok_or_else(|| eco_format!("version bound is missing major version"))?;
509        let minor = next("minor")?;
510        let patch = next("patch")?;
511        if let Some(rest) = parts.next() {
512            Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
513        }
514
515        Ok(Self { major, minor, patch })
516    }
517}
518
519impl Debug for VersionBound {
520    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
521        Display::fmt(self, f)
522    }
523}
524
525impl Display for VersionBound {
526    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
527        write!(f, "{}", self.major)?;
528        if let Some(minor) = self.minor {
529            write!(f, ".{minor}")?;
530        }
531        if let Some(patch) = self.patch {
532            write!(f, ".{patch}")?;
533        }
534        Ok(())
535    }
536}
537
538impl Serialize for VersionBound {
539    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
540        s.collect_str(self)
541    }
542}
543
544impl<'de> Deserialize<'de> for VersionBound {
545    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
546        let string = EcoString::deserialize(d)?;
547        string.parse().map_err(serde::de::Error::custom)
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use std::str::FromStr;
554
555    use super::*;
556
557    #[test]
558    fn version_version_match() {
559        let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
560
561        assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
562        assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
563        assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
564
565        assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
566        assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
567        assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
568
569        assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
570        assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
571        assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
572    }
573
574    #[test]
575    fn minimal_manifest() {
576        assert_eq!(
577            toml::from_str::<PackageManifest>(
578                r#"
579                [package]
580                name = "package"
581                version = "0.1.0"
582                entrypoint = "src/lib.typ"
583            "#
584            ),
585            Ok(PackageManifest {
586                package: PackageInfo::new(
587                    "package",
588                    PackageVersion { major: 0, minor: 1, patch: 0 },
589                    "src/lib.typ"
590                ),
591                template: None,
592                tool: ToolInfo { sections: BTreeMap::new() },
593                unknown_fields: BTreeMap::new(),
594            })
595        );
596    }
597
598    #[test]
599    fn tool_section() {
600        // NOTE: tool section must be table of tables, but we can't easily
601        // compare the error structurally
602        assert!(
603            toml::from_str::<PackageManifest>(
604                r#"
605                [package]
606                name = "package"
607                version = "0.1.0"
608                entrypoint = "src/lib.typ"
609
610                [tool]
611                not-table = "str"
612            "#
613            )
614            .is_err()
615        );
616
617        #[derive(Debug, PartialEq, Serialize, Deserialize)]
618        struct MyTool {
619            key: EcoString,
620        }
621
622        let mut manifest: PackageManifest = toml::from_str(
623            r#"
624            [package]
625            name = "package"
626            version = "0.1.0"
627            entrypoint = "src/lib.typ"
628
629            [tool.my-tool]
630            key = "value"
631        "#,
632        )
633        .unwrap();
634
635        let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
636        let my_tool = MyTool::deserialize(my_tool).unwrap();
637
638        assert_eq!(my_tool, MyTool { key: "value".into() });
639    }
640
641    #[test]
642    fn unknown_keys() {
643        let manifest: PackageManifest = toml::from_str(
644            r#"
645            [package]
646            name = "package"
647            version = "0.1.0"
648            entrypoint = "src/lib.typ"
649
650            [unknown]
651        "#,
652        )
653        .unwrap();
654
655        assert!(manifest.unknown_fields.contains_key("unknown"));
656    }
657}