summaryrefslogtreecommitdiff
path: root/crates/typst-syntax
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-03-06 12:33:35 +0100
committerGitHub <noreply@github.com>2024-03-06 11:33:35 +0000
commita558fd232b379e527eac2d5196eb9367b7d28032 (patch)
tree1e91330df3b21bb86e067067cc9ff07ac8d513c8 /crates/typst-syntax
parent898367f096fef507488438e00caae8c4ea1d0ff4 (diff)
Add `typst init` command (#3544)
Diffstat (limited to 'crates/typst-syntax')
-rw-r--r--crates/typst-syntax/src/file.rs236
-rw-r--r--crates/typst-syntax/src/lib.rs5
-rw-r--r--crates/typst-syntax/src/package.rs267
-rw-r--r--crates/typst-syntax/src/path.rs94
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)
+ }
+}