diff --git a/Cargo.lock b/Cargo.lock index 9f9fd4c50..a92c0d228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2836,6 +2836,7 @@ dependencies = [ "ecow", "once_cell", "serde", + "toml", "typst-utils", "unicode-ident", "unicode-math-class", diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index 816f0d340..e9c399920 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -17,6 +17,7 @@ typst-utils = { workspace = true } ecow = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } +toml = { workspace = true } unicode-ident = { workspace = true } unicode-math-class = { workspace = true } unicode-script = { workspace = true } diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index fb0031c85..c96aebe01 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -1,37 +1,101 @@ //! Package manifest parsing. +use std::collections::BTreeMap; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use ecow::{eco_format, EcoString}; +use serde::de::IgnoredAny; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use unscanny::Scanner; use crate::is_ident; +/// A type alias for a map of key-value pairs used to collect unknown fields +/// where values are completely discarded. +pub type UnknownFields = BTreeMap; + /// A parsed package manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +/// +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, 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")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub template: Option, + /// The tools section for third-party configuration. + #[serde(default)] + pub tool: ToolInfo, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, +} + +/// The `[tool]` key in the manifest. This field can be used to retrieve +/// 3rd-party tool configuration. +/// +// # Examples +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// # use ecow::EcoString; +/// # use typst_syntax::package::PackageManifest; +/// #[derive(Debug, PartialEq, Serialize, Deserialize)] +/// struct MyTool { +/// key: EcoString, +/// } +/// +/// let mut manifest: PackageManifest = toml::from_str(r#" +/// [package] +/// name = "package" +/// version = "0.1.0" +/// entrypoint = "src/lib.typ" +/// +/// [tool.my-tool] +/// key = "value" +/// "#)?; +/// +/// let my_tool = manifest +/// .tool +/// .sections +/// .remove("my-tool") +/// .ok_or("tool.my-tool section missing")?; +/// let my_tool = MyTool::deserialize(my_tool)?; +/// +/// assert_eq!(my_tool, MyTool { key: "value".into() }); +/// # Ok::<_, Box>(()) +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ToolInfo { + /// Any fields parsed in the tool section. + #[serde(flatten)] + pub sections: BTreeMap, } /// The `[template]` key in the manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +/// +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TemplateInfo { - /// The path of the starting point within the package. + /// The directory within the package that contains the files that should be + /// copied into the user's new project directory. pub path: EcoString, - /// The path of the entrypoint relative to the starting point's `path`. + /// A path relative to the template's path that points to the file serving + /// as the compilation target. pub entrypoint: EcoString, + /// A path relative to the package's root that points to a PNG or lossless + /// WebP thumbnail for the template. + pub thumbnail: EcoString, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, } /// 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)] +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PackageInfo { /// The name of the package within its namespace. pub name: EcoString, @@ -39,8 +103,42 @@ pub struct PackageInfo { pub version: PackageVersion, /// The path of the entrypoint into the package. pub entrypoint: EcoString, + /// A list of the package's authors. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub authors: Vec, + /// The package's license. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub license: Option, + /// A short description of the package. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A link to the package's web presence. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub homepage: Option, + /// A link to the repository where this package is developed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// An array of search keywords for the package. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub keywords: Vec, + /// An array with up to three of the predefined categories to help users + /// discover the package. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub categories: Vec, + /// An array of disciplines defining the target audience for which the + /// package is useful. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub disciplines: Vec, /// The minimum required compiler version for the package. + #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler: Option, + /// An array of globs specifying files that should not be part of the + /// published bundle. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub exclude: Vec, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, } impl PackageManifest { @@ -423,4 +521,97 @@ mod tests { assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap())); assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap())); } + + #[test] + fn minimal_manifest() { + assert_eq!( + toml::from_str::( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + "# + ), + Ok(PackageManifest { + package: PackageInfo { + name: "package".into(), + version: PackageVersion { major: 0, minor: 1, patch: 0 }, + entrypoint: "src/lib.typ".into(), + authors: vec![], + license: None, + description: None, + homepage: None, + repository: None, + keywords: vec![], + categories: vec![], + disciplines: vec![], + compiler: None, + exclude: vec![], + unknown_fields: BTreeMap::new(), + }, + template: None, + tool: ToolInfo { sections: BTreeMap::new() }, + unknown_fields: BTreeMap::new(), + }) + ); + } + + #[test] + fn tool_section() { + // NOTE: tool section must be table of tables, but we can't easily + // compare the error structurally + assert!(toml::from_str::( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [tool] + not-table = "str" + "# + ) + .is_err()); + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct MyTool { + key: EcoString, + } + + let mut manifest: PackageManifest = toml::from_str( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [tool.my-tool] + key = "value" + "#, + ) + .unwrap(); + + let my_tool = manifest.tool.sections.remove("my-tool").unwrap(); + let my_tool = MyTool::deserialize(my_tool).unwrap(); + + assert_eq!(my_tool, MyTool { key: "value".into() }); + } + + #[test] + fn unknown_keys() { + let manifest: PackageManifest = toml::from_str( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [unknown] + "#, + ) + .unwrap(); + + assert!(manifest.unknown_fields.contains_key("unknown")); + } }