diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-03-06 12:33:35 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-03-06 11:33:35 +0000 |
| commit | a558fd232b379e527eac2d5196eb9367b7d28032 (patch) | |
| tree | 1e91330df3b21bb86e067067cc9ff07ac8d513c8 /crates/typst-syntax/src | |
| parent | 898367f096fef507488438e00caae8c4ea1d0ff4 (diff) | |
Add `typst init` command (#3544)
Diffstat (limited to 'crates/typst-syntax/src')
| -rw-r--r-- | crates/typst-syntax/src/file.rs | 236 | ||||
| -rw-r--r-- | crates/typst-syntax/src/lib.rs | 5 | ||||
| -rw-r--r-- | crates/typst-syntax/src/package.rs | 267 | ||||
| -rw-r--r-- | crates/typst-syntax/src/path.rs | 94 |
4 files changed, 368 insertions, 234 deletions
diff --git a/crates/typst-syntax/src/file.rs b/crates/typst-syntax/src/file.rs index 6699f05d..b76cb9e3 100644 --- a/crates/typst-syntax/src/file.rs +++ b/crates/typst-syntax/src/file.rs @@ -1,16 +1,13 @@ //! File and package management. use std::collections::HashMap; -use std::fmt::{self, Debug, Display, Formatter}; -use std::path::{Component, Path, PathBuf}; -use std::str::FromStr; +use std::fmt::{self, Debug, Formatter}; use std::sync::RwLock; -use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::is_ident; +use crate::package::PackageSpec; +use crate::VirtualPath; /// The global package-path interner. static INTERNER: Lazy<RwLock<Interner>> = @@ -116,230 +113,3 @@ impl Debug for FileId { } } } - -/// An absolute path in the virtual file system of a project or package. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct VirtualPath(PathBuf); - -impl VirtualPath { - /// Create a new virtual path. - /// - /// Even if it doesn't start with `/` or `\`, it is still interpreted as - /// starting from the root. - pub fn new(path: impl AsRef<Path>) -> Self { - Self::new_impl(path.as_ref()) - } - - /// Non generic new implementation. - fn new_impl(path: &Path) -> Self { - let mut out = Path::new(&Component::RootDir).to_path_buf(); - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir => {} - Component::CurDir => {} - Component::ParentDir => match out.components().next_back() { - Some(Component::Normal(_)) => { - out.pop(); - } - _ => out.push(component), - }, - Component::Normal(_) => out.push(component), - } - } - Self(out) - } - - /// Create a virtual path from a real path and a real root. - /// - /// Returns `None` if the file path is not contained in the root (i.e. if - /// `root` is not a lexical prefix of `path`). No file system operations are - /// performed. - pub fn within_root(path: &Path, root: &Path) -> Option<Self> { - path.strip_prefix(root).ok().map(Self::new) - } - - /// Get the underlying path with a leading `/` or `\`. - pub fn as_rooted_path(&self) -> &Path { - &self.0 - } - - /// Get the underlying path without a leading `/` or `\`. - pub fn as_rootless_path(&self) -> &Path { - self.0.strip_prefix(Component::RootDir).unwrap_or(&self.0) - } - - /// Resolve the virtual path relative to an actual file system root - /// (where the project or package resides). - /// - /// Returns `None` if the path lexically escapes the root. The path might - /// still escape through symlinks. - pub fn resolve(&self, root: &Path) -> Option<PathBuf> { - let root_len = root.as_os_str().len(); - let mut out = root.to_path_buf(); - for component in self.0.components() { - match component { - Component::Prefix(_) => {} - Component::RootDir => {} - Component::CurDir => {} - Component::ParentDir => { - out.pop(); - if out.as_os_str().len() < root_len { - return None; - } - } - Component::Normal(_) => out.push(component), - } - } - Some(out) - } - - /// Resolve a path relative to this virtual path. - pub fn join(&self, path: impl AsRef<Path>) -> Self { - if let Some(parent) = self.0.parent() { - Self::new(parent.join(path)) - } else { - Self::new(path) - } - } -} - -impl Debug for VirtualPath { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(&self.0.display(), f) - } -} - -/// 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<Self, Self::Err> { - let mut s = unscanny::Scanner::new(s); - 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"))?; - } - - 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"))?; - } - - s.eat_if(':'); - - let version = s.after(); - if version.is_empty() { - Err("package specification is missing version")?; - } - - Ok(Self { - namespace: namespace.into(), - name: name.into(), - version: version.parse()?, - }) - } -} - -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) - } -} - -/// 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<Self, Self::Err> { - 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::<u32>() - .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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { - s.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for PackageVersion { - fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> { - let string = EcoString::deserialize(d)?; - string.parse().map_err(serde::de::Error::custom) - } -} diff --git a/crates/typst-syntax/src/lib.rs b/crates/typst-syntax/src/lib.rs index d93a8264..0ddb1460 100644 --- a/crates/typst-syntax/src/lib.rs +++ b/crates/typst-syntax/src/lib.rs @@ -1,6 +1,7 @@ //! Parser and syntax tree for Typst. pub mod ast; +pub mod package; mod file; mod highlight; @@ -8,12 +9,13 @@ mod kind; mod lexer; mod node; mod parser; +mod path; mod reparser; mod set; mod source; mod span; -pub use self::file::{FileId, PackageSpec, PackageVersion, VirtualPath}; +pub use self::file::FileId; pub use self::highlight::{highlight, highlight_html, Tag}; pub use self::kind::SyntaxKind; pub use self::lexer::{ @@ -21,6 +23,7 @@ pub use self::lexer::{ }; pub use self::node::{LinkedChildren, LinkedNode, SyntaxError, SyntaxNode}; pub use self::parser::{parse, parse_code, parse_math}; +pub use self::path::VirtualPath; pub use self::source::Source; pub use self::span::{Span, Spanned}; 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<TemplateInfo>, +} + +/// 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<PackageVersion>, +} + +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<Self, Self::Err> { + 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<Self, Self::Err> { + 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<PackageVersion, EcoString> { + 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<Self, Self::Err> { + 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::<u32>() + .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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for PackageVersion { + fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> { + let string = EcoString::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} diff --git a/crates/typst-syntax/src/path.rs b/crates/typst-syntax/src/path.rs new file mode 100644 index 00000000..b561128c --- /dev/null +++ b/crates/typst-syntax/src/path.rs @@ -0,0 +1,94 @@ +use std::fmt::{self, Debug, Display, Formatter}; +use std::path::{Component, Path, PathBuf}; + +/// An absolute path in the virtual file system of a project or package. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct VirtualPath(PathBuf); + +impl VirtualPath { + /// Create a new virtual path. + /// + /// Even if it doesn't start with `/` or `\`, it is still interpreted as + /// starting from the root. + pub fn new(path: impl AsRef<Path>) -> Self { + Self::new_impl(path.as_ref()) + } + + /// Non generic new implementation. + fn new_impl(path: &Path) -> Self { + let mut out = Path::new(&Component::RootDir).to_path_buf(); + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => {} + Component::CurDir => {} + Component::ParentDir => match out.components().next_back() { + Some(Component::Normal(_)) => { + out.pop(); + } + _ => out.push(component), + }, + Component::Normal(_) => out.push(component), + } + } + Self(out) + } + + /// Create a virtual path from a real path and a real root. + /// + /// Returns `None` if the file path is not contained in the root (i.e. if + /// `root` is not a lexical prefix of `path`). No file system operations are + /// performed. + pub fn within_root(path: &Path, root: &Path) -> Option<Self> { + path.strip_prefix(root).ok().map(Self::new) + } + + /// Get the underlying path with a leading `/` or `\`. + pub fn as_rooted_path(&self) -> &Path { + &self.0 + } + + /// Get the underlying path without a leading `/` or `\`. + pub fn as_rootless_path(&self) -> &Path { + self.0.strip_prefix(Component::RootDir).unwrap_or(&self.0) + } + + /// Resolve the virtual path relative to an actual file system root + /// (where the project or package resides). + /// + /// Returns `None` if the path lexically escapes the root. The path might + /// still escape through symlinks. + pub fn resolve(&self, root: &Path) -> Option<PathBuf> { + let root_len = root.as_os_str().len(); + let mut out = root.to_path_buf(); + for component in self.0.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => {} + Component::CurDir => {} + Component::ParentDir => { + out.pop(); + if out.as_os_str().len() < root_len { + return None; + } + } + Component::Normal(_) => out.push(component), + } + } + Some(out) + } + + /// Resolve a path relative to this virtual path. + pub fn join(&self, path: impl AsRef<Path>) -> Self { + if let Some(parent) = self.0.parent() { + Self::new(parent.join(path)) + } else { + Self::new(path) + } + } +} + +impl Debug for VirtualPath { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(&self.0.display(), f) + } +} |
