diff --git a/src/check.rs b/src/check.rs index 2779ac1..79e9ae7 100644 --- a/src/check.rs +++ b/src/check.rs @@ -39,7 +39,7 @@ pub async fn all_checks( compile::check(&mut template_diags, &template_world); let template_dir = template_world .root() - .strip_prefix(worlds.package.root()) + .relative_template_dir() .expect("Template should be in a subfolder of the package"); diags.extend(template_diags, template_dir); } diff --git a/src/check/authors.rs b/src/check/authors.rs index 844808d..52a0f05 100644 --- a/src/check/authors.rs +++ b/src/check/authors.rs @@ -27,7 +27,7 @@ pub async fn check(diags: &mut Diagnostics, spec: &PackageSpec) -> Option<()> { } pub async fn commit_for_previous_version(spec: &PackageSpec) -> Option { - let last_manifest = spec.previous_version()?.directory().join("typst.toml"); + let last_manifest = spec.previous_version()?.git_dir().join("typst.toml"); let repo = git::repo_dir(); let repo = git::GitRepo::open(&repo).await.ok()?; @@ -36,8 +36,8 @@ pub async fn commit_for_previous_version(spec: &PackageSpec) -> Option { } pub async fn authors_are_differents(spec: &PackageSpec) -> Option { - let last_manifest = spec.previous_version()?.directory().join("typst.toml"); - let new_manifest = spec.directory().join("typst.toml"); + let last_manifest = spec.previous_version()?.git_dir().join("typst.toml"); + let new_manifest = spec.git_dir().join("typst.toml"); let repo = git::repo_dir(); let repo = git::GitRepo::open(&repo).await.ok()?; diff --git a/src/check/imports.rs b/src/check/imports.rs index bc06b80..7ad877b 100644 --- a/src/check/imports.rs +++ b/src/check/imports.rs @@ -1,16 +1,9 @@ -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use codespan_reporting::diagnostic::{Diagnostic, Severity}; -use typst::{ - World, - syntax::{ - ast::{self, AstNode, ModuleImport}, - package::{PackageSpec, PackageVersion, VersionlessPackageSpec}, - }, -}; +use typst::syntax::ast::{self, AstNode, ModuleImport}; +use typst::syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use walkdir::WalkDir; use crate::check::path::PackagePath; @@ -18,15 +11,8 @@ use crate::check::{Diagnostics, Result, TryExt, label}; use crate::world::SystemWorld; pub fn check(diags: &mut Diagnostics, package_dir: &Path, world: &SystemWorld) -> Result<()> { - let root_path = world.root(); - let main_path = root_path - .join(world.main().vpath().get_without_slash()) - .canonicalize() - .ok(); - let all_packages = root_path - .parent() - .and_then(|package_dir| package_dir.parent()) - .and_then(|namespace_dir| namespace_dir.parent()); + let all_packages = world.root().all_packages(); + let entrypoint = world.root().is_package().then(|| world.entrypoint()); for ch in WalkDir::new(package_dir).into_iter().flatten() { let Ok(meta) = ch.metadata() else { @@ -46,7 +32,7 @@ pub fn check(diags: &mut Diagnostics, package_dir: &Path, world: &SystemWorld) - world, source.root(), path.full(), - main_path.as_deref(), + entrypoint.as_deref(), all_packages, ); } @@ -58,12 +44,12 @@ pub fn check(diags: &mut Diagnostics, package_dir: &Path, world: &SystemWorld) - pub fn check_ast( diags: &mut Diagnostics, world: &SystemWorld, - root: &typst::syntax::SyntaxNode, + node: &typst::syntax::SyntaxNode, path: &Path, - main_path: Option<&Path>, + package_entrypoint: Option<&Path>, all_packages: Option<&Path>, ) { - let imports = root.children().filter_map(|ch| ch.cast::()); + let imports = node.children().filter_map(|ch| ch.cast::()); for import in imports { let ast::Expr::Str(source_str) = import.source() else { continue; @@ -74,7 +60,7 @@ pub fn check_ast( .join(source_str.get().as_str()) .canonicalize() .ok(); - if main_path == import_path.as_deref() { + if package_entrypoint == import_path.as_deref() { diags.emit( Diagnostic::warning() .with_labels(label(world, import.span()).into_iter().collect()) diff --git a/src/check/manifest.rs b/src/check/manifest.rs index 5b44b3b..a8ca8e7 100644 --- a/src/check/manifest.rs +++ b/src/check/manifest.rs @@ -13,7 +13,8 @@ use typst::syntax::{ }; use crate::check::files; -use crate::check::path::{self, PackagePath}; +use crate::check::path::PackagePath; +use crate::world::WorldRoot; use crate::{ check::{Diagnostics, Result, TryExt}, world::SystemWorld, @@ -34,7 +35,7 @@ pub struct Manifest { #[derive(Debug, Clone)] pub struct Package { - pub entrypoint: Spanned, + pub entrypoint: Spanned, pub name: Option>, pub version: Option>, pub exclude: Spanned, @@ -45,7 +46,7 @@ pub struct Template { pub path: Option>, /// The package directory of this path is still relative to the package, not /// the template directory. - pub entrypoint: Option>, + pub entrypoint: Option>, pub thumbnail: Option>, } @@ -82,7 +83,15 @@ pub async fn check( "manifest/package/entrypoint/missing", "Packages must specify an `entrypoint` in their manifest", )?; - let entrypoint = entrypoint.map(|e| PackagePath::from_relative(package_dir, e)); + let entrypoint = entrypoint.try_map(|path| { + VirtualPath::new(path).map_err(|err| { + Diagnostic::error() + .with_label(Label::primary(manifest_id(), entrypoint.span())) + .with_code("manifest/template/entrypoint/invalid") + .with_message(format_args!("invalid entrypoint ({err})")) + }) + })?; + let name = check_name(diags, package, package_spec); let version = check_version(diags, package, package_spec); let template = check_template(diags, &manifest, package_dir); @@ -121,14 +130,10 @@ pub async fn check( }); let world = SystemWorld::new( - package.entrypoint.full().to_owned(), - package_dir.to_owned(), + package.entrypoint.val.clone(), + WorldRoot::Package(package_dir.to_owned()), package_spec.clone(), - ) - .error( - "compile/package-world-init", - "Failed to initialize the Typst compiler", - )?; + ); let template_world = world_for_template(package_dir, package_spec, &package, &template); @@ -162,7 +167,7 @@ fn check_name( let error = Diagnostic::error().with_label(Label::primary(manifest_id(), name.span())); let warning = Diagnostic::warning().with_label(Label::primary(manifest_id(), name.span())); - let Some(name) = name.try_map(Item::as_str) else { + let Some(name) = name.filter_map(Item::as_str) else { diags.emit( error .with_code("manifest/package/name/type") @@ -228,7 +233,7 @@ fn check_version( let error = Diagnostic::error().with_label(Label::primary(manifest_id(), version.span())); - let Some(version) = version.try_map(Item::as_str) else { + let Some(version) = version.filter_map(Item::as_str) else { diags.emit( error .with_code("manifest/package/version/type") @@ -237,7 +242,7 @@ fn check_version( return None; }; - let Some(version) = version.try_map(|v| v.parse::().ok()) else { + let Some(version) = version.filter_map(|v| v.parse::().ok()) else { diags.emit( error .with_code("manifest/package/version/invalid") @@ -272,7 +277,7 @@ fn check_version( fn check_compiler_version(diags: &mut Diagnostics, package: Spanned<&Table>) -> Option<()> { let compiler = package.get_spanned("compiler")?; - let Some(compiler) = compiler.try_map(Item::as_str) else { + let Some(compiler) = compiler.filter_map(Item::as_str) else { diags.emit( Diagnostic::error() .with_label(Label::primary(manifest_id(), compiler.span())) @@ -527,7 +532,7 @@ fn check_exclude( return Ok(Spanned::new(Exclude::empty(), package.span())); }; - let exclude = exclude.try_map(Item::as_array).error( + let exclude = exclude.filter_map(Item::as_array).error( "manifest/package/exclude/type", "`exclude` must be an array of strings", )?; @@ -599,6 +604,10 @@ impl Exclude { Self { globs } } + pub fn root(&self) -> &Path { + self.globs.path() + } + /// Whether the package file path is excluded. pub fn matches_file>(&self, path: &PackagePath) -> bool { self.matches_relative_file(path.relative()) @@ -634,7 +643,7 @@ fn check_template( ) -> Option> { let template = manifest.get_spanned("template")?; - let Some(template) = template.try_map(Item::as_table) else { + let Some(template) = template.filter_map(Item::as_table) else { diags.emit( Diagnostic::error() .with_label(Label::primary(manifest_id(), template.span())) @@ -650,12 +659,17 @@ fn check_template( }); let entrypoint = template.get_str("entrypoint").and_then(|entrypoint| { - let template_dir = path.as_ref()?; - Some( - entrypoint - .map(|entrypoint| path::join_to(template_dir.full(), entrypoint)) - .map(|path| PackagePath::from_full(package_dir, path)), - ) + let virtual_path = VirtualPath::new(entrypoint.val) + .inspect_err(|err| { + diags.emit( + Diagnostic::error() + .with_label(Label::primary(manifest_id(), entrypoint.span())) + .with_code("manifest/template/entrypoint/invalid") + .with_message(format_args!("invalid entrypoint ({err})")), + ); + }) + .ok()?; + Some(entrypoint.map(|_| virtual_path)) }); let thumbnail = template.get_str("thumbnail").map(|path| { @@ -678,18 +692,17 @@ fn world_for_template( ) -> Option { let package_spec = package_spec?; let template = template.as_ref()?; - let template_path = template.path.as_ref()?; - let template_main = template.entrypoint.as_ref()?; - - let mut world = SystemWorld::new( - template_main.full().to_owned(), - template_path.full().to_owned(), - Some(package_spec), - ) - .ok()? - .with_package_override(package_dir.to_owned()); - world.exclude(package.exclude.val.clone()); - Some(world) + let template_dir = template.path.as_ref()?; + let main = template.entrypoint.as_ref()?; + + let root = WorldRoot::Template { + package: package_dir.to_owned(), + template: template_dir.full().to_owned(), + }; + + let world = SystemWorld::new(main.val.clone(), root, Some(package_spec)); + + Some(world.exclude(package.exclude.val.clone())) } fn dont_exclude_template_files( @@ -820,7 +833,7 @@ impl Spanned { } } - pub fn try_map(self, f: F) -> Option> + pub fn filter_map(self, f: F) -> Option> where F: FnOnce(T) -> Option, { @@ -829,6 +842,16 @@ impl Spanned { span: self.span, }) } + + pub fn try_map(self, f: F) -> std::result::Result, E> + where + F: FnOnce(T) -> std::result::Result, + { + Ok(Spanned { + val: f(self.val)?, + span: self.span, + }) + } } impl Spanned<&T> { @@ -877,14 +900,14 @@ impl TomlExt for Table { } fn get_table(&self, name: &'static str) -> Option> { - self.get_spanned(name)?.try_map(Item::as_table) + self.get_spanned(name)?.filter_map(Item::as_table) } fn get_array(&self, name: &'static str) -> Option> { - self.get_spanned(name)?.try_map(Item::as_array) + self.get_spanned(name)?.filter_map(Item::as_array) } fn get_str(&self, name: &'static str) -> Option> { - self.get_spanned(name)?.try_map(Item::as_str) + self.get_spanned(name)?.filter_map(Item::as_str) } } diff --git a/src/check/readme.rs b/src/check/readme.rs index 4fc35fa..8e94c26 100644 --- a/src/check/readme.rs +++ b/src/check/readme.rs @@ -6,12 +6,8 @@ use codespan_reporting::diagnostic::{Diagnostic, Label}; use comrak::nodes::{LineColumn, NodeList, NodeValue as MdNode, Sourcepos}; use html5ever::tendril::TendrilSink; use regex::Regex; -use typst::syntax::RootedPath; -use typst::{ - World, - foundations::Bytes, - syntax::{FileId, VirtualPath}, -}; +use typst::foundations::Bytes; +use typst::syntax::{FileId, RootedPath, VirtualPath}; use url::Url; use crate::check::path::PackagePath; @@ -29,7 +25,7 @@ pub struct Readme { pub async fn check(world: &SystemWorld, diags: &mut Diagnostics) -> crate::check::Result { // check syntax, versions and kebab-case // warn on unsupported gfm features - let text = tokio::fs::read_to_string(world.root().join("README.md")) + let text = tokio::fs::read_to_string(world.root().package_dir().join("README.md")) .await .error("io/readme", "Failed to read README.md")?; @@ -166,22 +162,14 @@ fn check_readme_code_block( kebab_case::check_ast(world, diags, &HashSet::new(), source.root(), true); - let main_path = world - .root() - .join(world.main().vpath().get_without_slash()) - .canonicalize() - .ok(); - let all_packages = world - .root() - .parent() - .and_then(|package_dir| package_dir.parent()) - .and_then(|namespace_dir| namespace_dir.parent()); + let entrypoint = world.root().is_package().then(|| world.entrypoint()); + let all_packages = world.root().all_packages(); imports::check_ast( diags, world, source.root(), - &world.root().join("README.md"), - main_path.as_deref(), + &world.root().package_dir().join("README.md"), + entrypoint.as_deref(), all_packages, ); } @@ -311,7 +299,7 @@ fn check_readme_link_url( } // Check if the local file exists. - let path = PackagePath::from_relative(world.root(), absolute_path.as_ref()); + let path = PackagePath::from_relative(world.root().package_dir(), absolute_path.as_ref()); if !path.full().exists() { diags.emit( Diagnostic::error() diff --git a/src/cli.rs b/src/cli.rs index 0cbc10c..26ea709 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,15 +12,15 @@ use crate::{check::all_checks, package::PackageExt, world::SystemWorld}; pub async fn main(spec_or_path: String, json_output: bool, offline: bool) { let package_spec: Option = spec_or_path.parse().ok(); let package_dir = if let Some(ref package_spec) = package_spec { - package_spec.directory() + package_spec.git_dir() } else { PathBuf::from(spec_or_path) }; match all_checks(package_spec.as_ref(), package_dir, true, offline).await { - Ok((mut world, diags)) => { + Ok((world, diags)) => { if let Err(err) = - print_diagnostics(&mut world, diags.errors(), diags.warnings(), json_output) + print_diagnostics(world, diags.errors(), diags.warnings(), json_output) { error!("failed to print diagnostics ({err})"); error!( @@ -47,7 +47,7 @@ pub async fn main(spec_or_path: String, json_output: bool, offline: bool) { /// Print diagnostic messages to the terminal. pub fn print_diagnostics( - world: &mut SystemWorld, + world: SystemWorld, errors: &[Diagnostic], warnings: &[Diagnostic], json: bool, @@ -60,16 +60,16 @@ pub fn print_diagnostics( // We should be able to print diagnostics even on excluded files. If we // don't remove the exclusion, it will fail to read and display the file // contents. - world.exclude(Exclude::empty()); + let mut world = world.exclude(Exclude::empty()); for diagnostic in errors.iter().chain(warnings).rev() { if json { - json::emit(&mut std::io::stdout(), world, diagnostic)?; + json::emit(&mut std::io::stdout(), &mut world, diagnostic)?; } else { term::emit_to_write_style( &mut term::termcolor::StandardStream::stdout(term::termcolor::ColorChoice::Auto), &config, - world, + &world, diagnostic, )?; } diff --git a/src/package.rs b/src/package.rs index cea818d..05299f1 100644 --- a/src/package.rs +++ b/src/package.rs @@ -4,24 +4,29 @@ use typst::syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec use crate::github::git; -/// Return the path of the directory containing all the packages (i.e. `typst/packages/packages`). -fn dir() -> PathBuf { +/// Return the path of the directory within the current git directory containing +/// all the packages (i.e. `typst/packages/packages`). +fn git_packages_dir() -> PathBuf { git::repo_dir().join("packages") } pub trait PackageExt: Sized { type Versionless; + /// Resolves the directory of the previous version of the package inside the + /// current git directory. fn previous_version(&self) -> Option; - fn directory(&self) -> PathBuf; + /// Resolves the directory of the package version inside the current git + /// directory. + fn git_dir(&self) -> PathBuf; } impl PackageExt for PackageSpec { type Versionless = VersionlessPackageSpec; fn previous_version(&self) -> Option { - let all_versions_dir = self.versionless().directory(); + let all_versions_dir = self.versionless().git_dir(); let mut last_version = None; for version_dir in std::fs::read_dir(&all_versions_dir).ok()? { let Ok(version_dir) = version_dir else { @@ -52,8 +57,8 @@ impl PackageExt for PackageSpec { }) } - fn directory(&self) -> PathBuf { - dir() + fn git_dir(&self) -> PathBuf { + git_packages_dir() .join(self.namespace.as_str()) .join(self.name.as_str()) .join(self.version.to_string()) @@ -61,11 +66,14 @@ impl PackageExt for PackageSpec { } pub trait VersionlessPackageExt { - fn directory(&self) -> PathBuf; + /// Resolves the directory of the package inside the current git directory. + fn git_dir(&self) -> PathBuf; } impl VersionlessPackageExt for VersionlessPackageSpec { - fn directory(&self) -> PathBuf { - dir().join(self.namespace.as_str()).join(self.name.as_str()) + fn git_dir(&self) -> PathBuf { + git_packages_dir() + .join(self.namespace.as_str()) + .join(self.name.as_str()) } } diff --git a/src/world.rs b/src/world.rs index 48858f4..f3eedd6 100644 --- a/src/world.rs +++ b/src/world.rs @@ -12,7 +12,7 @@ use fontdb::Database; use parking_lot::Mutex; use tracing::{Level, debug, span}; use typst::foundations::Duration; -use typst::syntax::{RootedPath, VirtualRoot}; +use typst::syntax::{RealizeError, RootedPath, VirtualRoot}; use typst::{ Library, LibraryExt, World, diag::{FileError, FileResult, PackageError, PackageResult}, @@ -28,7 +28,7 @@ use crate::package::PackageExt; /// A world that provides access to the operating system. pub struct SystemWorld { /// The root relative to which absolute paths are resolved. - root: PathBuf, + root: WorldRoot, /// The input path. main: FileId, /// Typst's standard library. @@ -45,30 +45,21 @@ pub struct SystemWorld { now: typst_kit::datetime::Time, /// The package specification of the currently checked package. package_spec: Option, - /// Override for package resolution - package_override: Option, /// Files that are considered excluded and should not be read from. exclude: Exclude, } impl SystemWorld { /// Create a new system world. - pub fn new( - input: PathBuf, - root: PathBuf, - package_spec: Option, - ) -> Result { - // Resolve the virtual path of the main file within the project root. - let main_path = - VirtualPath::virtualize(&root, &input).or(Err(WorldCreationError::InputOutsideRoot))?; - let main = FileId::new(RootedPath::new(VirtualRoot::Project, main_path)); + pub fn new(input: VirtualPath, root: WorldRoot, package_spec: Option) -> Self { + let main = FileId::new(RootedPath::new(VirtualRoot::Project, input)); let library = Library::default(); let mut searcher = FontSearcher::new(); searcher.search(&[]); - Ok(Self { + Self { root, main, library: LazyHash::new(library), @@ -77,31 +68,33 @@ impl SystemWorld { slots: Mutex::new(HashMap::new()), now: typst_kit::datetime::Time::system(), package_spec, - package_override: None, exclude: Exclude::empty(), - }) + } } - pub fn with_package_override(mut self, dir: PathBuf) -> Self { - self.package_override = Some(dir); + pub fn exclude(mut self, exclude: Exclude) -> Self { + self.exclude = exclude; self } /// The root relative to which absolute paths are resolved. - pub fn root(&self) -> &Path { + pub fn root(&self) -> &WorldRoot { &self.root } + /// Get the realized entrypoint path. + pub fn entrypoint(&self) -> PathBuf { + self.root() + .realize(self.main().vpath()) + .expect("main file to be inside the world root") + } + /// Lookup a source file by id. #[track_caller] pub fn lookup(&self, id: FileId) -> FileResult { self.source(id) } - pub fn exclude(&mut self, exclude: Exclude) { - self.exclude = exclude; - } - pub fn virtual_source(&self, id: FileId, src: Bytes, line_shift: usize) -> FileResult { self.slot(id, |slot| slot.virtual_source(src, line_shift)) } @@ -113,12 +106,6 @@ impl SystemWorld { pub fn package_spec(&self) -> Option<&PackageSpec> { self.package_spec.as_ref() } - - pub fn package_override(&self) -> Option<(&PackageSpec, &Path)> { - self.package_spec - .as_ref() - .zip(self.package_override.as_deref()) - } } impl World for SystemWorld { @@ -136,13 +123,13 @@ impl World for SystemWorld { fn source(&self, id: FileId) -> FileResult { self.slot(id, |slot| { - slot.source(&self.root, self.package_override(), &self.exclude) + slot.source(&self.root, self.package_spec.as_ref(), &self.exclude) }) } fn file(&self, id: FileId) -> FileResult { self.slot(id, |slot| { - slot.file(&self.root, self.package_override(), &self.exclude) + slot.file(&self.root, self.package_spec.as_ref(), &self.exclude) }) } @@ -166,6 +153,61 @@ impl SystemWorld { } } +pub enum WorldRoot { + Package(PathBuf), + Template { package: PathBuf, template: PathBuf }, +} + +impl WorldRoot { + pub fn is_package(&self) -> bool { + matches!(self, Self::Package(..)) + } + + /// Returns the directory of the package this world lives in. + pub fn package_dir(&self) -> &Path { + match self { + WorldRoot::Package(path) => path, + WorldRoot::Template { package, .. } => package, + } + } + + /// Returns the root of the world, either the template directory if this is + /// a template root, otherwise the package directory. + pub fn world_dir(&self) -> &Path { + match self { + WorldRoot::Package(path) => path, + WorldRoot::Template { template, .. } => template, + } + } + + /// Returns the relative template dir. + pub fn relative_template_dir(&self) -> Option<&Path> { + match self { + WorldRoot::Package(_) => None, + WorldRoot::Template { package, template } => { + Some(template.strip_prefix(package).unwrap()) + } + } + } + + /// Returns the path in which all pacakges reside. + pub fn all_packages(&self) -> Option<&Path> { + // 1. version + // 2. package name + // 3. namespace + self.package_dir().parent()?.parent()?.parent() + } + + /// Virtualize a path relative to this world root. + pub fn realize(&self, path: &VirtualPath) -> Result { + let root = match self { + WorldRoot::Package(path) => path, + WorldRoot::Template { template, .. } => template, + }; + path.realize(root) + } +} + /// Holds the processed data for a file ID. /// /// Both fields can be populated if the file is both imported and read(). @@ -193,12 +235,12 @@ impl FileSlot { /// Retrieve the source for this file. fn source( &mut self, - project_root: &Path, - package_override: Option<(&PackageSpec, &Path)>, + root: &WorldRoot, + override_spec: Option<&PackageSpec>, exclude: &Exclude, ) -> FileResult { self.source.get_or_init( - || read(self.id, project_root, package_override, exclude), + || read(root, override_spec, exclude, self.id), |data, prev| { let text = decode_utf8(&data)?; if let Some(mut prev) = prev { @@ -230,12 +272,12 @@ impl FileSlot { /// Retrieve the file's bytes. fn file( &mut self, - project_root: &Path, - package_override: Option<(&PackageSpec, &Path)>, + root: &WorldRoot, + override_spec: Option<&PackageSpec>, exclude: &Exclude, ) -> FileResult { self.file.get_or_init( - || read(self.id, project_root, package_override, exclude), + || read(root, override_spec, exclude, self.id), |data, _| Ok(Bytes::new(data)), ) } @@ -293,89 +335,118 @@ impl SlotCell { } } -/// Resolves the path of a file id on the system, downloading a package if -/// necessary. -fn system_path( - package_override: Option<(&PackageSpec, &Path)>, - project_root: &Path, +/// Reads a file from a `FileId`. +fn read( + root: &WorldRoot, + override_spec: Option<&PackageSpec>, + exclude: &Exclude, + id: FileId, +) -> FileResult> { + let resolved = resolve_system_path(root, override_spec, exclude, id)?; + read_from_disk(&resolved) +} + +/// Resolves the path of a file id on the system. +fn resolve_system_path( + root: &WorldRoot, + override_spec: Option<&PackageSpec>, exclude: &Exclude, id: FileId, ) -> FileResult { let _ = span!(Level::DEBUG, "Path resolution").enter(); debug!("File ID = {:?}", id); - let exclude = |id: FileId, root: &Path| { - let file = id.vpath().realize(root).or(Err(FileError::AccessDenied))?; - - if exclude.matches_relative_file(id.vpath().get_without_slash()) { - debug!("This file is excluded"); - return Err(FileError::Other(Some( - "This file exists but is excluded from your package.".into(), - ))); - } - debug!("Resolved to {}", file.display()); - Ok(file) + // Determine the root path relative to which the file path will be resolved. + let mut resolved = None; + let root = match id.root() { + VirtualRoot::Project => root.world_dir(), + VirtualRoot::Package(spec) => { + // If the current package is imported, return the package dir + // directly, otherwise try to find the package: + // 1. relative to the current world's package dir. + // 2. inside the current git directory. + // 3. in the global package cache. + if Some(spec) == override_spec { + root.package_dir() + } else if let Some(dir) = find_relative_package(root, spec) { + resolved.insert(dir) + } else { + let dir = find_in_git_dir_or_cache(spec).map_err(FileError::Package)?; + resolved.insert(dir) + } + } }; - // Determine the root path relative to which the file path - // will be resolved. - let root = if let VirtualRoot::Package(spec) = id.root() { - if let Some((override_spec, override_dir)) = package_override - && spec == override_spec - { - return exclude(id, override_dir); - } + let realized_path = id.vpath().realize(root).or(Err(FileError::AccessDenied))?; - expect_parents( - project_root, - &[&spec.version.to_string(), &spec.name, &spec.namespace], - ) - .map(|packages_root| { - Ok(packages_root - .join(spec.namespace.as_str()) - .join(spec.name.as_str()) - .join(spec.version.to_string())) - }) - .unwrap_or_else(|| prepare_package(spec)) - .map_err(FileError::Package)? - } else { - project_root.to_owned() - }; - exclude(id, &root) + // FIXME: To be fully correct, we would need to read the exclude globs + // of the imported package and filter out files that are imported but + // excluded. Though these issues will most likely be discovered during + // package development. + if let Ok(exclude_relative_path) = realized_path.strip_prefix(exclude.root()) + && exclude.matches_relative_file(exclude_relative_path) + { + debug!("This file is excluded"); + return Err(FileError::Other(Some( + "This file exists but is excluded from your package.".into(), + ))); + } + + debug!("Resolved to {}", realized_path.display()); + Ok(realized_path) } // Goes up in a file system hierarchy while the parent folder matches the expected name -fn expect_parents<'a>(dir: &'a Path, parents: &'a [&'a str]) -> Option { - let dir = dir.canonicalize().ok()?; +fn find_relative_package(root: &WorldRoot, spec: &PackageSpec) -> Option { + let dir = root.all_packages()?; + let mut buf = dir.to_path_buf(); - if parents.is_empty() { - return Some(dir); - } + buf.push(spec.namespace.as_str()); + buf.push(spec.name.as_str()); + buf.push(spec.version.to_string()); - let (expected_parent, rest) = parents.split_first()?; - if dir.file_name().and_then(|n| n.to_str()) != Some(expected_parent) { + if !buf.exists() { debug!( - "Expected parent folder to be {}, but it was {}", - expected_parent, + "Expected package `{spec}` to be present in `{}`", dir.display() ); return None; } - expect_parents(dir.parent()?, rest) + Some(buf) } -/// Reads a file from a `FileId`. -/// -/// If the ID represents stdin it will read from standard input, -/// otherwise it gets the file path of the ID and reads the file from disk. -fn read( - id: FileId, - project_root: &Path, - package_override: Option<(&PackageSpec, &Path)>, - exclude: &Exclude, -) -> FileResult> { - read_from_disk(&system_path(package_override, project_root, exclude, id)?) +/// Try to find a pacakge in current git directory or in the on-disk cache. +fn find_in_git_dir_or_cache(spec: &PackageSpec) -> PackageResult { + let git_package_dir = spec.git_dir(); + if git_package_dir.exists() { + return Ok(git_package_dir); + } + + let subdir = format!( + "typst/packages/{}/{}/{}", + spec.namespace, spec.name, spec.version + ); + + if let Some(data_dir) = dirs::data_dir() { + let dir = data_dir.join(&subdir); + if dir.exists() { + return Ok(dir); + } + } + + if let Some(cache_dir) = dirs::cache_dir() { + let dir = cache_dir.join(&subdir); + if dir.exists() { + return Ok(dir); + } + + return Err(PackageError::NetworkFailed(Some( + "All packages are supposed to be present in the `packages` repository, or in the local cache.".into(), + ))); + } + + Err(PackageError::NotFound(spec.clone())) } /// Read a file from disk. @@ -395,22 +466,6 @@ fn decode_utf8(buf: &[u8]) -> FileResult<&str> { buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf), )?) } -/// An error that occurs during world construction. -#[derive(Debug)] -pub enum WorldCreationError { - /// The input file is not contained within the root folder. - InputOutsideRoot, -} - -impl std::fmt::Display for WorldCreationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WorldCreationError::InputOutsideRoot => { - write!(f, "source file must be contained in project root") - } - } - } -} /// Searches for fonts. pub struct FontSearcher { @@ -505,36 +560,3 @@ impl FontSearcher { } } } - -/// Make a package available in the on-disk cache. -pub fn prepare_package(spec: &PackageSpec) -> PackageResult { - let subdir = format!( - "typst/packages/{}/{}/{}", - spec.namespace, spec.name, spec.version - ); - - let local_package_dir = spec.directory(); - if local_package_dir.exists() { - return Ok(local_package_dir); - } - - if let Some(data_dir) = dirs::data_dir() { - let dir = data_dir.join(&subdir); - if dir.exists() { - return Ok(dir); - } - } - - if let Some(cache_dir) = dirs::cache_dir() { - let dir = cache_dir.join(&subdir); - if dir.exists() { - return Ok(dir); - } - - return Err(PackageError::NetworkFailed(Some( - "All packages are supposed to be present in the `packages` repository, or in the local cache.".into(), - ))); - } - - Err(PackageError::NotFound(spec.clone())) -} diff --git a/tests/local/owl-lst-thesis/0.1.0/LICENSE b/tests/local/owl-lst-thesis/0.1.0/LICENSE new file mode 100644 index 0000000..30c6295 --- /dev/null +++ b/tests/local/owl-lst-thesis/0.1.0/LICENSE @@ -0,0 +1,16 @@ +MIT No Attribution + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/local/owl-lst-thesis/0.1.0/README.md b/tests/local/owl-lst-thesis/0.1.0/README.md new file mode 100644 index 0000000..19e35fa --- /dev/null +++ b/tests/local/owl-lst-thesis/0.1.0/README.md @@ -0,0 +1,13 @@ +# Typst style for LST theses + +This is a Typst thesis style +for the [Department of Language Science and Technology](https://www.lst.uni-saarland.de/) +at [Saarland University](https://www.uni-saarland.de/). +You can use this for your Bachelor's or Master's Thesis or any other document you like. +The template is maintained by [Alexander Koller](https://www.coli.uni-saarland.de/~koller/). + +The template sets up the title page, declaration, abstract, acknowledgments, +table of contents, chapter styling, headers, page numbers, figure captions, and bibliography +formatting for an LST thesis. +The default `screen` mode uses equal left and right margins. The `print` mode is intended for +double-sided printing and binding, with a wider inner margin. diff --git a/tests/local/owl-lst-thesis/0.1.0/lib.typ b/tests/local/owl-lst-thesis/0.1.0/lib.typ new file mode 100644 index 0000000..9d24f59 --- /dev/null +++ b/tests/local/owl-lst-thesis/0.1.0/lib.typ @@ -0,0 +1,2 @@ +#let lst-template-version = "0.1.0" +#let prepared-string = "Prepared with the owl-lst-thesis Typst template, version " + lst-template-version + "." diff --git a/tests/local/owl-lst-thesis/0.1.0/template/main.typ b/tests/local/owl-lst-thesis/0.1.0/template/main.typ new file mode 100644 index 0000000..ad73262 --- /dev/null +++ b/tests/local/owl-lst-thesis/0.1.0/template/main.typ @@ -0,0 +1,16 @@ +#import "@local/owl-lst-thesis:0.1.0": * + +// Use "de" for German template labels. +#set text(lang: "en") + += Introduction + +Start writing your thesis here. + += Background + +Many interesting combinatorial problems are NP-complete. + += My Contribution + += Conclusion diff --git a/tests/local/owl-lst-thesis/0.1.0/typst.toml b/tests/local/owl-lst-thesis/0.1.0/typst.toml new file mode 100644 index 0000000..facfb4f --- /dev/null +++ b/tests/local/owl-lst-thesis/0.1.0/typst.toml @@ -0,0 +1,28 @@ +[package] +name = "owl-lst-thesis" +version = "0.1.0" +entrypoint = "lib.typ" +authors = ["Alexander Koller "] +license = "MIT-0" +description = "Thesis in the LST department at Saarland University." +repository = "https://github.com/coli-saar/typst-lst-template" +keywords = [ + "Saarland University", + "Language Science and Technology", + "LST", + "thesis", +] +categories = ["thesis"] +disciplines = ["computer-science", "linguistics"] +compiler = "0.15.0" +exclude = [ + "/tmp/", + "/custom.bib", + "/main.typ", + "/main.pdf", + ".DS_Store", +] + +[template] +path = "template" +entrypoint = "main.typ" diff --git a/tests/ref/local+owl-lst-thesis+0.1.0.txt b/tests/ref/local+owl-lst-thesis+0.1.0.txt new file mode 100644 index 0000000..e69de29