Add typst init
command (#3544)
This commit is contained in:
parent
898367f096
commit
a558fd232b
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -741,6 +741,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@ -2607,6 +2613,7 @@ dependencies = [
|
||||
"env_proxy",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
"fs_extra",
|
||||
"native-tls",
|
||||
"notify",
|
||||
"once_cell",
|
||||
@ -2624,6 +2631,7 @@ dependencies = [
|
||||
"siphasher 1.0.0",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"typst",
|
||||
"typst-assets",
|
||||
"typst-macros",
|
||||
|
@ -46,6 +46,7 @@ ecow = { version = "0.2", features = ["serde"] }
|
||||
env_proxy = "0.4"
|
||||
flate2 = "1"
|
||||
fontdb = { version = "0.16", default-features = false }
|
||||
fs_extra = "1.3"
|
||||
hayagriva = "0.5.1"
|
||||
heck = "0.4"
|
||||
hypher = "0.1.4"
|
||||
@ -115,7 +116,7 @@ unicode-properties = "0.1"
|
||||
unicode-script = "0.5"
|
||||
unicode-segmentation = "1"
|
||||
unscanny = "0.1"
|
||||
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip"] }
|
||||
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
|
||||
usvg = { version = "0.38.0", default-features = false, features = ["text"] }
|
||||
walkdir = "2"
|
||||
wasmi = "0.31.0"
|
||||
|
@ -37,6 +37,7 @@ ecow = { workspace = true }
|
||||
env_proxy = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
fontdb = { workspace = true, features = ["memmap", "fontconfig"] }
|
||||
fs_extra = { workspace = true }
|
||||
native-tls = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
@ -53,6 +54,7 @@ serde_yaml = { workspace = true }
|
||||
siphasher = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
xz2 = { workspace = true, optional = true }
|
||||
zip = { workspace = true, optional = true }
|
||||
@ -78,7 +80,7 @@ default = ["embed-fonts"]
|
||||
embed-fonts = []
|
||||
|
||||
# Permits the CLI to update itself without a package manager.
|
||||
self-update = ["dep:self-replace", "dep:xz2", "dep:zip", "ureq/json"]
|
||||
self-update = ["dep:self-replace", "dep:xz2", "dep:zip"]
|
||||
|
||||
# Whether to vendor OpenSSL. Not applicable to Windows and macOS builds.
|
||||
vendor-openssl = ["openssl/vendored"]
|
||||
|
@ -46,6 +46,9 @@ pub enum Command {
|
||||
#[command(visible_alias = "w")]
|
||||
Watch(CompileCommand),
|
||||
|
||||
/// Initializes a new project from a template
|
||||
Init(InitCommand),
|
||||
|
||||
/// Processes an input file to extract provided metadata
|
||||
Query(QueryCommand),
|
||||
|
||||
@ -89,6 +92,21 @@ pub struct CompileCommand {
|
||||
pub timings: Option<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Initializes a new project from a template
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct InitCommand {
|
||||
/// The template to use, e.g. `@preview/charged-ieee`
|
||||
///
|
||||
/// You can specify the version by appending e.g. `:0.1.0`. If no version is
|
||||
/// specified, Typst will default to the latest version.
|
||||
///
|
||||
/// Supports both local and published templates.
|
||||
pub template: String,
|
||||
|
||||
/// The project directory, defaults to the template's name
|
||||
pub dir: Option<String>,
|
||||
}
|
||||
|
||||
/// Processes an input file to extract provided metadata
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct QueryCommand {
|
||||
|
@ -180,12 +180,12 @@ impl RemoteReader {
|
||||
(remaining / speed) as u64
|
||||
}));
|
||||
writeln!(
|
||||
&mut terminal::out(),
|
||||
terminal::out(),
|
||||
"{total_downloaded} / {download_size} ({percent:3.0} %) {speed_h} in {elapsed} ETA: {eta}",
|
||||
)?;
|
||||
}
|
||||
None => writeln!(
|
||||
&mut terminal::out(),
|
||||
terminal::out(),
|
||||
"Total downloaded: {total_downloaded} Speed: {speed_h} Elapsed: {elapsed}",
|
||||
)?,
|
||||
};
|
||||
|
114
crates/typst-cli/src/init.rs
Normal file
114
crates/typst-cli/src/init.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use codespan_reporting::term::termcolor::{Color, ColorSpec, WriteColor};
|
||||
use ecow::eco_format;
|
||||
use fs_extra::dir::CopyOptions;
|
||||
use typst::diag::{bail, FileError, StrResult};
|
||||
use typst::syntax::package::{
|
||||
PackageManifest, PackageSpec, TemplateInfo, VersionlessPackageSpec,
|
||||
};
|
||||
|
||||
use crate::args::InitCommand;
|
||||
|
||||
/// Execute an initialization command.
|
||||
pub fn init(command: &InitCommand) -> StrResult<()> {
|
||||
// Parse the package specification. If the user didn't specify the version,
|
||||
// we try to figure it out automatically by downloading the package index
|
||||
// or searching the disk.
|
||||
let spec: PackageSpec = command.template.parse().or_else(|err| {
|
||||
// Try to parse without version, but prefer the error message of the
|
||||
// normal package spec parsing if it fails.
|
||||
let spec: VersionlessPackageSpec = command.template.parse().map_err(|_| err)?;
|
||||
let version = crate::package::determine_latest_version(&spec)?;
|
||||
StrResult::Ok(spec.at(version))
|
||||
})?;
|
||||
|
||||
// Find or download the package.
|
||||
let package_path = crate::package::prepare_package(&spec)?;
|
||||
|
||||
// Parse the manifest.
|
||||
let manifest = parse_manifest(&package_path)?;
|
||||
manifest.validate(&spec)?;
|
||||
|
||||
// Ensure that it is indeed a template.
|
||||
let Some(template) = &manifest.template else {
|
||||
bail!("package {spec} is not a template");
|
||||
};
|
||||
|
||||
// Determine the directory at which we will create the project.
|
||||
let project_dir = Path::new(command.dir.as_deref().unwrap_or(&manifest.package.name));
|
||||
|
||||
// Set up the project.
|
||||
scaffold_project(project_dir, &package_path, template)?;
|
||||
|
||||
// Print the summary.
|
||||
print_summary(spec, project_dir, template).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the manifest of the package located at `package_path`.
|
||||
fn parse_manifest(package_path: &Path) -> StrResult<PackageManifest> {
|
||||
let toml_path = package_path.join("typst.toml");
|
||||
let string = std::fs::read_to_string(&toml_path).map_err(|err| {
|
||||
eco_format!(
|
||||
"failed to read package manifest ({})",
|
||||
FileError::from_io(err, &toml_path)
|
||||
)
|
||||
})?;
|
||||
|
||||
toml::from_str(&string)
|
||||
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
||||
}
|
||||
|
||||
/// Creates the project directory with the template's contents and returns the
|
||||
/// path at which it was created.
|
||||
fn scaffold_project(
|
||||
project_dir: &Path,
|
||||
package_path: &Path,
|
||||
template: &TemplateInfo,
|
||||
) -> StrResult<()> {
|
||||
if project_dir.exists() {
|
||||
bail!("project directory already exists (at {})", project_dir.display());
|
||||
}
|
||||
|
||||
let template_dir = package_path.join(template.path.as_str());
|
||||
if !template_dir.exists() {
|
||||
bail!("template directory does not exist (at {})", template_dir.display());
|
||||
}
|
||||
|
||||
fs_extra::dir::copy(
|
||||
&template_dir,
|
||||
project_dir,
|
||||
&CopyOptions::new().content_only(true),
|
||||
)
|
||||
.map_err(|err| eco_format!("failed to create project directory ({err})"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints a summary after successful initialization.
|
||||
fn print_summary(
|
||||
spec: PackageSpec,
|
||||
project_dir: &Path,
|
||||
template: &TemplateInfo,
|
||||
) -> std::io::Result<()> {
|
||||
let mut gray = ColorSpec::new();
|
||||
gray.set_fg(Some(Color::White));
|
||||
gray.set_dimmed(true);
|
||||
|
||||
let mut out = crate::terminal::out();
|
||||
writeln!(out, "Successfully created new project from {spec} 🎉")?;
|
||||
writeln!(out, "To start writing, run:")?;
|
||||
out.set_color(&gray)?;
|
||||
write!(out, "> ")?;
|
||||
out.reset()?;
|
||||
writeln!(out, "cd {}", project_dir.display())?;
|
||||
out.set_color(&gray)?;
|
||||
write!(out, "> ")?;
|
||||
out.reset()?;
|
||||
writeln!(out, "typst watch {}", template.entrypoint)?;
|
||||
writeln!(out)?;
|
||||
Ok(())
|
||||
}
|
@ -2,6 +2,7 @@ mod args;
|
||||
mod compile;
|
||||
mod download;
|
||||
mod fonts;
|
||||
mod init;
|
||||
mod package;
|
||||
mod query;
|
||||
mod terminal;
|
||||
@ -39,6 +40,7 @@ fn main() -> ExitCode {
|
||||
let res = match &ARGS.command {
|
||||
Command::Compile(command) => crate::compile::compile(timer, command.clone()),
|
||||
Command::Watch(command) => crate::watch::watch(timer, command.clone()),
|
||||
Command::Init(command) => crate::init::init(command),
|
||||
Command::Query(command) => crate::query::query(command),
|
||||
Command::Fonts(command) => crate::fonts::fonts(command),
|
||||
Command::Update(command) => crate::update::update(command),
|
||||
|
@ -5,12 +5,16 @@ use std::path::{Path, PathBuf};
|
||||
use codespan_reporting::term::{self, termcolor};
|
||||
use ecow::eco_format;
|
||||
use termcolor::WriteColor;
|
||||
use typst::diag::{PackageError, PackageResult};
|
||||
use typst::syntax::PackageSpec;
|
||||
use typst::diag::{bail, PackageError, PackageResult, StrResult};
|
||||
use typst::syntax::package::{
|
||||
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
|
||||
};
|
||||
|
||||
use crate::download::download_with_progress;
|
||||
use crate::download::{download, download_with_progress};
|
||||
use crate::terminal;
|
||||
|
||||
const HOST: &str = "https://packages.typst.org";
|
||||
|
||||
/// Make a package available in the on-disk cache.
|
||||
pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> {
|
||||
let subdir =
|
||||
@ -25,30 +29,59 @@ pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> {
|
||||
|
||||
if let Some(cache_dir) = dirs::cache_dir() {
|
||||
let dir = cache_dir.join(&subdir);
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" && !dir.exists() {
|
||||
download_package(spec, &dir)?;
|
||||
}
|
||||
|
||||
if dir.exists() {
|
||||
return Ok(dir);
|
||||
}
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" {
|
||||
download_package(spec, &dir)?;
|
||||
if dir.exists() {
|
||||
return Ok(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(PackageError::NotFound(spec.clone()))
|
||||
}
|
||||
|
||||
/// Try to determine the latest version of a package.
|
||||
pub fn determine_latest_version(
|
||||
spec: &VersionlessPackageSpec,
|
||||
) -> StrResult<PackageVersion> {
|
||||
if spec.namespace == "preview" {
|
||||
// For `@preview`, download the package index and find the latest
|
||||
// version.
|
||||
download_index()?
|
||||
.iter()
|
||||
.filter(|package| package.name == spec.name)
|
||||
.map(|package| package.version)
|
||||
.max()
|
||||
.ok_or_else(|| eco_format!("failed to find package {spec}"))
|
||||
} else {
|
||||
// For other namespaces, search locally. We only search in the data
|
||||
// directory and not the cache directory, because the latter is not
|
||||
// intended for storage of local packages.
|
||||
let subdir = format!("typst/packages/{}/{}", spec.namespace, spec.name);
|
||||
dirs::data_dir()
|
||||
.into_iter()
|
||||
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
|
||||
.flatten()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.map(|entry| entry.path())
|
||||
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
|
||||
.max()
|
||||
.ok_or_else(|| eco_format!("please specify the desired version"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Download a package over the network.
|
||||
fn download_package(spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
|
||||
// The `@preview` namespace is the only namespace that supports on-demand
|
||||
// fetching.
|
||||
assert_eq!(spec.namespace, "preview");
|
||||
|
||||
let url = format!(
|
||||
"https://packages.typst.org/preview/{}-{}.tar.gz",
|
||||
spec.name, spec.version
|
||||
);
|
||||
let url = format!("{HOST}/preview/{}-{}.tar.gz", spec.name, spec.version);
|
||||
|
||||
print_downloading(spec).unwrap();
|
||||
|
||||
@ -67,14 +100,28 @@ fn download_package(spec: &PackageSpec, package_dir: &Path) -> PackageResult<()>
|
||||
})
|
||||
}
|
||||
|
||||
/// Download the `@preview` package index.
|
||||
fn download_index() -> StrResult<Vec<PackageInfo>> {
|
||||
let url = format!("{HOST}/preview/index.json");
|
||||
match download(&url) {
|
||||
Ok(response) => response
|
||||
.into_json()
|
||||
.map_err(|err| eco_format!("failed to parse package index: {err}")),
|
||||
Err(ureq::Error::Status(404, _)) => {
|
||||
bail!("failed to fetch package index (not found)")
|
||||
}
|
||||
Err(err) => bail!("failed to fetch package index ({err})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print that a package downloading is happening.
|
||||
fn print_downloading(spec: &PackageSpec) -> io::Result<()> {
|
||||
let styles = term::Styles::default();
|
||||
|
||||
let mut term_out = terminal::out();
|
||||
term_out.set_color(&styles.header_help)?;
|
||||
write!(term_out, "downloading")?;
|
||||
let mut out = terminal::out();
|
||||
out.set_color(&styles.header_help)?;
|
||||
write!(out, "downloading")?;
|
||||
|
||||
term_out.reset()?;
|
||||
writeln!(term_out, " {spec}")
|
||||
out.reset()?;
|
||||
writeln!(out, " {spec}")
|
||||
}
|
||||
|
@ -110,9 +110,9 @@ impl Release {
|
||||
};
|
||||
|
||||
match download(&url) {
|
||||
Ok(response) => response
|
||||
.into_json()
|
||||
.map_err(|err| eco_format!("unable to parse JSON response: {err}")),
|
||||
Ok(response) => response.into_json().map_err(|err| {
|
||||
eco_format!("failed to parse release information ({err})")
|
||||
}),
|
||||
Err(ureq::Error::Status(404, _)) => {
|
||||
bail!("release not found (searched at {url})")
|
||||
}
|
||||
|
@ -273,27 +273,27 @@ impl Status {
|
||||
let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
|
||||
let color = self.color();
|
||||
|
||||
let mut term_out = terminal::out();
|
||||
term_out.clear_screen()?;
|
||||
let mut out = terminal::out();
|
||||
out.clear_screen()?;
|
||||
|
||||
term_out.set_color(&color)?;
|
||||
write!(term_out, "watching")?;
|
||||
term_out.reset()?;
|
||||
out.set_color(&color)?;
|
||||
write!(out, "watching")?;
|
||||
out.reset()?;
|
||||
match &command.common.input {
|
||||
Input::Stdin => writeln!(term_out, " <stdin>"),
|
||||
Input::Path(path) => writeln!(term_out, " {}", path.display()),
|
||||
Input::Stdin => writeln!(out, " <stdin>"),
|
||||
Input::Path(path) => writeln!(out, " {}", path.display()),
|
||||
}?;
|
||||
|
||||
term_out.set_color(&color)?;
|
||||
write!(term_out, "writing to")?;
|
||||
term_out.reset()?;
|
||||
writeln!(term_out, " {}", output.display())?;
|
||||
out.set_color(&color)?;
|
||||
write!(out, "writing to")?;
|
||||
out.reset()?;
|
||||
writeln!(out, " {}", output.display())?;
|
||||
|
||||
writeln!(term_out)?;
|
||||
writeln!(term_out, "[{timestamp}] {}", self.message())?;
|
||||
writeln!(term_out)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "[{timestamp}] {}", self.message())?;
|
||||
writeln!(out)?;
|
||||
|
||||
term_out.flush()
|
||||
out.flush()
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
|
@ -19,7 +19,6 @@ use typst_timing::{timed, TimingScope};
|
||||
use crate::args::{Input, SharedArgs};
|
||||
use crate::compile::ExportCache;
|
||||
use crate::fonts::{FontSearcher, FontSlot};
|
||||
use crate::package::prepare_package;
|
||||
|
||||
/// Static `FileId` allocated for stdin.
|
||||
/// This is to ensure that a file is read in the correct way.
|
||||
@ -344,7 +343,7 @@ fn system_path(project_root: &Path, id: FileId) -> FileResult<PathBuf> {
|
||||
let buf;
|
||||
let mut root = project_root;
|
||||
if let Some(spec) = id.package() {
|
||||
buf = prepare_package(spec)?;
|
||||
buf = crate::package::prepare_package(spec)?;
|
||||
root = &buf;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
267
crates/typst-syntax/src/package.rs
Normal file
267
crates/typst-syntax/src/package.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
94
crates/typst-syntax/src/path.rs
Normal file
94
crates/typst-syntax/src/path.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -9,7 +9,8 @@ use std::string::FromUtf8Error;
|
||||
use comemo::Tracked;
|
||||
use ecow::{eco_vec, EcoVec};
|
||||
|
||||
use crate::syntax::{PackageSpec, Span, Spanned, SyntaxError};
|
||||
use crate::syntax::package::PackageSpec;
|
||||
use crate::syntax::{Span, Spanned, SyntaxError};
|
||||
use crate::{World, WorldExt};
|
||||
|
||||
/// Early-return with a [`StrResult`] or [`SourceResult`].
|
||||
@ -497,7 +498,7 @@ pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString
|
||||
}
|
||||
roxmltree::Error::DuplicatedAttribute(attr, pos) => {
|
||||
eco_format!(
|
||||
"failed to parse {format}: (duplicate attribute '{attr}' in line {})",
|
||||
"failed to parse {format} (duplicate attribute '{attr}' in line {})",
|
||||
pos.row
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
use comemo::TrackedMut;
|
||||
use ecow::{eco_format, eco_vec, EcoString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::diag::{
|
||||
bail, error, warning, At, FileError, SourceResult, StrResult, Trace, Tracepoint,
|
||||
};
|
||||
use crate::diag::{bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint};
|
||||
use crate::eval::{eval, Eval, Vm};
|
||||
use crate::foundations::{Content, Module, Value};
|
||||
use crate::syntax::ast::{self, AstNode};
|
||||
use crate::syntax::{FileId, PackageSpec, PackageVersion, Span, VirtualPath};
|
||||
use crate::syntax::package::{PackageManifest, PackageSpec};
|
||||
use crate::syntax::{FileId, Span, VirtualPath};
|
||||
use crate::World;
|
||||
|
||||
impl Eval for ast::ModuleImport<'_> {
|
||||
@ -136,7 +134,10 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
|
||||
// Evaluate the manifest.
|
||||
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
||||
let bytes = vm.world().file(manifest_id).at(span)?;
|
||||
let manifest = PackageManifest::parse(&bytes).at(span)?;
|
||||
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
|
||||
let manifest: PackageManifest = toml::from_str(string)
|
||||
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
||||
.at(span)?;
|
||||
manifest.validate(&spec).at(span)?;
|
||||
|
||||
// Evaluate the entry point.
|
||||
@ -175,61 +176,3 @@ fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
|
||||
)
|
||||
.trace(world, point, span)
|
||||
}
|
||||
|
||||
/// A parsed package manifest.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
struct PackageManifest {
|
||||
/// Details about the package itself.
|
||||
package: PackageInfo,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
struct PackageInfo {
|
||||
/// The name of the package within its namespace.
|
||||
name: EcoString,
|
||||
/// The package's version.
|
||||
version: PackageVersion,
|
||||
/// The path of the entrypoint into the package.
|
||||
entrypoint: EcoString,
|
||||
/// The minimum required compiler version for the package.
|
||||
compiler: Option<PackageVersion>,
|
||||
}
|
||||
|
||||
impl PackageManifest {
|
||||
/// Parse the manifest from raw bytes.
|
||||
fn parse(bytes: &[u8]) -> StrResult<Self> {
|
||||
let string = std::str::from_utf8(bytes).map_err(FileError::from)?;
|
||||
toml::from_str(string).map_err(|err| {
|
||||
eco_format!("package manifest is malformed: {}", err.message())
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure that this manifest is indeed for the specified package.
|
||||
fn validate(&self, spec: &PackageSpec) -> StrResult<()> {
|
||||
if self.package.name != spec.name {
|
||||
bail!("package manifest contains mismatched name `{}`", self.package.name);
|
||||
}
|
||||
|
||||
if self.package.version != spec.version {
|
||||
bail!(
|
||||
"package manifest contains mismatched version {}",
|
||||
self.package.version
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(compiler) = self.package.compiler {
|
||||
let current = PackageVersion::compiler();
|
||||
if current < compiler {
|
||||
bail!(
|
||||
"package requires typst {compiler} or newer \
|
||||
(current version is {current})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,8 @@ use crate::foundations::{
|
||||
use crate::introspection::{Introspector, Locator};
|
||||
use crate::layout::{Alignment, Dir, LayoutRoot};
|
||||
use crate::model::Document;
|
||||
use crate::syntax::{FileId, PackageSpec, Source, Span};
|
||||
use crate::syntax::package::PackageSpec;
|
||||
use crate::syntax::{FileId, Source, Span};
|
||||
use crate::text::{Font, FontBook};
|
||||
use crate::visualize::Color;
|
||||
|
||||
|
@ -4,7 +4,8 @@ use std::ops::Range;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst::syntax::{PackageVersion, Source};
|
||||
use typst::syntax::package::PackageVersion;
|
||||
use typst::syntax::Source;
|
||||
use unscanny::Scanner;
|
||||
|
||||
/// Each test and subset may contain metadata.
|
||||
|
Loading…
x
Reference in New Issue
Block a user