1#![cfg(feature = "fs")]
14
15use std::collections::HashSet;
16use std::fmt;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use ecow::EcoVec;
22use typst::diag::{FileError, FileResult, SourceDiagnostic};
23use typst::foundations::{Bytes, Datetime, Dict, Duration};
24use typst::syntax::package::PackageSpec;
25use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
26use typst::text::{Font, FontBook};
27use typst::utils::LazyHash;
28use typst::{Feature, Library, LibraryExt, World};
29use typst_kit::files::{FileLoader, FileStore};
30use typst_kit::fonts::FontStore;
31
32use crate::creation::{
33 DiscoverySpecification, PackCreationError, PackCreationInput, PackCreationOutcome, create,
34};
35use crate::domain::{DocumentTime, TypstTarget};
36use crate::font_catalog::FontDisposition;
37use crate::fs_fonts::{FilesystemFontLimits, FilesystemFontSource, read_filesystem_fonts};
38use crate::fs_packages::{
39 FilesystemPackageAuthority, FilesystemPackageAuthorityReadError, FilesystemPackageLimits,
40 ReadPackages,
41};
42use crate::fs_project;
43use crate::manifest::PackMetadata;
44use crate::pack::Pack;
45use crate::package_catalog::{PackageCatalog, PackageCatalogError, PackageDisposition};
46use crate::package_failure::PackageReadFailures;
47use crate::project_snapshot::ProjectSnapshot;
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct FilesystemPackAssemblyProfile {
52 project: fs_project::FilesystemProjectLimits,
53 packages: FilesystemPackageLimits,
54 fonts: FilesystemFontLimits,
55 #[cfg(feature = "egress")]
56 package_expansion: crate::PackageExpansionLimits,
57}
58
59impl FilesystemPackAssemblyProfile {
60 pub const fn reference_v1() -> Self {
62 Self {
63 project: fs_project::FilesystemProjectLimits::reference_v1(),
64 packages: FilesystemPackageLimits::reference_v1(),
65 fonts: FilesystemFontLimits::reference_v1(),
66 #[cfg(feature = "egress")]
67 package_expansion: crate::PackageExpansionLimits::reference_v1(),
68 }
69 }
70
71 pub const fn project(&self) -> fs_project::FilesystemProjectLimits {
72 self.project
73 }
74
75 pub const fn packages(&self) -> FilesystemPackageLimits {
76 self.packages
77 }
78
79 pub const fn fonts(&self) -> FilesystemFontLimits {
80 self.fonts
81 }
82
83 #[cfg(feature = "egress")]
84 pub const fn package_expansion(&self) -> crate::PackageExpansionLimits {
85 self.package_expansion
86 }
87}
88
89#[derive(Clone, Copy, Debug, Default, PartialEq)]
91#[non_exhaustive]
92pub enum FilesystemPackAssemblyClock {
93 #[default]
95 System,
96 Fixed(DocumentTime),
98}
99
100#[derive(Debug)]
102pub struct FilesystemPackAssemblerConfig {
103 font_paths: Vec<PathBuf>,
104 system_fonts: bool,
105 typst_embedded_fonts: bool,
106 package_path: Option<PathBuf>,
107 package_cache_path: Option<PathBuf>,
108 offline: bool,
109 #[cfg(feature = "egress")]
110 certificate: Option<PathBuf>,
111 clock: FilesystemPackAssemblyClock,
112 profile: FilesystemPackAssemblyProfile,
113}
114
115impl FilesystemPackAssemblerConfig {
116 pub fn new() -> Self {
119 Self {
120 font_paths: Vec::new(),
121 system_fonts: true,
122 typst_embedded_fonts: true,
123 package_path: None,
124 package_cache_path: None,
125 offline: false,
126 #[cfg(feature = "egress")]
127 certificate: None,
128 clock: FilesystemPackAssemblyClock::System,
129 profile: FilesystemPackAssemblyProfile::reference_v1(),
130 }
131 }
132
133 pub fn profile(mut self, profile: FilesystemPackAssemblyProfile) -> Self {
135 self.profile = profile;
136 self
137 }
138
139 pub fn clock(mut self, clock: FilesystemPackAssemblyClock) -> Self {
141 self.clock = clock;
142 self
143 }
144
145 pub fn font_path(mut self, path: impl Into<PathBuf>) -> Self {
147 self.font_paths.push(path.into());
148 self
149 }
150
151 pub fn system_fonts(mut self, system: bool) -> Self {
153 self.system_fonts = system;
154 self
155 }
156
157 pub fn typst_embedded_fonts(mut self, include: bool) -> Self {
159 self.typst_embedded_fonts = include;
160 self
161 }
162
163 pub fn package_path(mut self, path: impl Into<PathBuf>) -> Self {
166 self.package_path = Some(path.into());
167 self
168 }
169
170 pub fn package_cache_path(mut self, path: impl Into<PathBuf>) -> Self {
175 self.package_cache_path = Some(path.into());
176 self
177 }
178
179 pub fn offline(mut self, offline: bool) -> Self {
188 self.offline = offline;
189 self
190 }
191
192 #[cfg(feature = "egress")]
197 pub fn certificate(mut self, path: Option<PathBuf>) -> Self {
198 self.certificate = path;
199 self
200 }
201}
202
203impl Default for FilesystemPackAssemblerConfig {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209pub struct FilesystemPackAssemblyRequest<'a> {
212 root: &'a Path,
213 entrypoint: &'a Path,
214 vendor_packages: bool,
215 embed_fonts: bool,
216 include_typst_embedded_fonts: bool,
217 inputs: Dict,
218 features: Vec<Feature>,
219 target: TypstTarget,
220 document_time: Option<DocumentTime>,
221 timings: Option<PathBuf>,
222 metadata: Option<PackMetadata>,
223}
224
225impl<'a> FilesystemPackAssemblyRequest<'a> {
226 pub fn new(root: &'a Path, entrypoint: &'a Path) -> Self {
228 Self {
229 root,
230 entrypoint,
231 vendor_packages: true,
232 embed_fonts: false,
233 include_typst_embedded_fonts: false,
234 inputs: Dict::new(),
235 features: Vec::new(),
236 target: TypstTarget::Paged,
237 document_time: None,
238 timings: None,
239 metadata: None,
240 }
241 }
242
243 pub fn vendor_packages(mut self, vendor: bool) -> Self {
245 self.vendor_packages = vendor;
246 self
247 }
248
249 pub fn embed_fonts(mut self, embed: bool) -> Self {
251 self.embed_fonts = embed;
252 self
253 }
254
255 pub fn include_typst_embedded_fonts(mut self, include: bool) -> Self {
257 self.include_typst_embedded_fonts = include;
258 self
259 }
260
261 pub fn inputs(mut self, inputs: Dict) -> Self {
263 self.inputs = inputs;
264 self
265 }
266
267 pub fn feature(mut self, feature: Feature) -> Self {
269 self.features.push(feature);
270 self
271 }
272
273 pub fn target(mut self, target: TypstTarget) -> Self {
275 self.target = target;
276 self
277 }
278
279 pub fn document_time(mut self, document_time: DocumentTime) -> Self {
281 self.document_time = Some(document_time);
282 self
283 }
284
285 pub fn timings(mut self, path: Option<PathBuf>) -> Self {
287 self.timings = path;
288 self
289 }
290
291 pub fn metadata(mut self, metadata: PackMetadata) -> Self {
293 self.metadata = Some(metadata);
294 self
295 }
296}
297
298pub struct FilesystemPackAssembler {
329 authority: FilesystemPackageAuthority,
330 font_paths: Vec<PathBuf>,
331 system_fonts: bool,
332 typst_embedded_fonts: bool,
333 clock: FilesystemPackAssemblyClock,
334 profile: FilesystemPackAssemblyProfile,
335 #[cfg(test)]
336 after_creation_hook: Option<Box<dyn Fn()>>,
337}
338
339impl FilesystemPackAssembler {
340 pub fn new(config: FilesystemPackAssemblerConfig) -> Self {
343 let authority = FilesystemPackageAuthority::with_limits(
344 config.package_path.as_deref(),
345 config.package_cache_path.as_deref(),
346 config.offline,
347 config.profile.packages,
348 #[cfg(feature = "egress")]
349 config.profile.package_expansion,
350 );
351 #[cfg(feature = "egress")]
352 let authority = authority.certificate(config.certificate);
353 Self {
354 authority,
355 font_paths: config.font_paths,
356 system_fonts: config.system_fonts,
357 typst_embedded_fonts: config.typst_embedded_fonts,
358 clock: config.clock,
359 profile: config.profile,
360 #[cfg(test)]
361 after_creation_hook: None,
362 }
363 }
364
365 #[cfg(test)]
366 pub(crate) fn after_creation_hook(mut self, hook: impl Fn() + 'static) -> Self {
367 self.after_creation_hook = Some(Box::new(hook));
368 self
369 }
370
371 pub fn assemble(
374 &self,
375 request: FilesystemPackAssemblyRequest<'_>,
376 ) -> Result<PackAssemblyReport, FilesystemPackAssemblyError> {
377 let (result, timing_error) = self.assemble_with_timing(request);
378 timing_error.map_or(result, Err)
379 }
380
381 #[doc(hidden)]
382 pub fn assemble_with_timing(
383 &self,
384 request: FilesystemPackAssemblyRequest<'_>,
385 ) -> (
386 Result<PackAssemblyReport, FilesystemPackAssemblyError>,
387 Option<FilesystemPackAssemblyError>,
388 ) {
389 let mut timing_error = None;
390 let result = self.assemble_inner(request, &mut timing_error);
391 (result, timing_error)
392 }
393
394 fn assemble_inner(
395 &self,
396 request: FilesystemPackAssemblyRequest<'_>,
397 timing_error: &mut Option<FilesystemPackAssemblyError>,
398 ) -> Result<PackAssemblyReport, FilesystemPackAssemblyError> {
399 let root = request.root.canonicalize().map_err(|err| {
400 FilesystemPackAssemblyError::io("failed to resolve project root", err)
401 })?;
402 let entrypoint_abs = if request.entrypoint.is_absolute() {
403 request.entrypoint.to_owned()
404 } else {
405 root.join(request.entrypoint)
406 };
407 let entrypoint_abs = entrypoint_abs
408 .canonicalize()
409 .map_err(|err| FilesystemPackAssemblyError::io("failed to resolve entrypoint", err))?;
410 let entrypoint = VirtualPath::virtualize(&root, &entrypoint_abs)
411 .map_err(|_| FilesystemPackAssemblyError::OutsideRoot(entrypoint_abs.clone()))?;
412 let snapshot = Arc::new(fs_project::read_filesystem_project(
413 &root,
414 entrypoint.get_without_slash(),
415 self.profile.project,
416 )?);
417
418 let packages = Arc::new(ReadPackages::new());
419
420 let scanned_disposition = FontDisposition::embedded_if(request.embed_fonts);
421 let mut font_sources = Vec::new();
422 if self.system_fonts {
423 font_sources.push(FilesystemFontSource::system(scanned_disposition));
424 }
425 #[cfg(feature = "embedded-fonts")]
426 if self.typst_embedded_fonts {
427 font_sources.push(FilesystemFontSource::typst_embedded(
428 FontDisposition::embedded_if(
429 request.embed_fonts && request.include_typst_embedded_fonts,
430 ),
431 ));
432 }
433 #[cfg(not(feature = "embedded-fonts"))]
434 let _ = (
435 self.typst_embedded_fonts,
436 request.include_typst_embedded_fonts,
437 );
438 font_sources.extend(
439 self.font_paths
440 .iter()
441 .map(|path| FilesystemFontSource::directory(path, scanned_disposition)),
442 );
443 let font_catalog = read_filesystem_fonts(font_sources, self.profile.fonts)?;
444
445 let document_time = request
448 .document_time
449 .unwrap_or_else(|| self.clock.document_time());
450
451 let discovery = DiscoverySpecification::new(
452 request.target,
453 request.inputs,
454 document_time,
455 request.features,
456 )
457 .map_err(|source| {
458 FilesystemPackAssemblyError::DiscoverySpecification(
459 FilesystemPackAssemblyDiscoveryError { source },
460 )
461 })?;
462
463 let mut world = ReadWorld {
464 root: root.clone(),
465 #[cfg(feature = "diagnostics")]
466 workdir: std::env::current_dir()
467 .ok()
468 .map(|path| path.canonicalize().unwrap_or(path)),
469 library: LazyHash::new(Library::builder().build()),
470 main: RootedPath::new(VirtualRoot::Project, entrypoint).intern(),
471 files: FileStore::new(ReadLoader {
472 project: Arc::clone(&snapshot),
473 packages: Arc::clone(&packages),
474 }),
475 fonts: FontStore::new(),
476 };
477
478 let disposition = if request.vendor_packages {
479 PackageDisposition::Embedded
480 } else {
481 PackageDisposition::External
482 };
483 let mut timer = typst_kit::timer::Timer::new_or_placeholder(request.timings);
484 let mut creation = None;
485 let timings = timer.record(&mut world, |_| {
486 creation = Some(resolve_and_create(
487 &snapshot,
488 &font_catalog,
489 &discovery,
490 request.metadata.as_ref(),
491 &self.authority,
492 &packages,
493 disposition,
494 ));
495 });
496 let Some(creation) = creation else {
497 return Err(FilesystemPackAssemblyError::Timings(
498 timings
499 .expect_err("timer did not execute creation")
500 .to_string(),
501 ));
502 };
503 *timing_error = timings
504 .err()
505 .map(|error| FilesystemPackAssemblyError::Timings(error.to_string()));
506 let (pack, warnings) = match creation {
507 Ok(created) => created,
508 Err(error) => return Err(error.into_assembly_error(world)),
509 };
510
511 #[cfg(test)]
512 if let Some(hook) = &self.after_creation_hook {
513 hook();
514 }
515
516 Ok(PackAssemblyReport {
517 pack,
518 warnings,
519 #[cfg(feature = "diagnostics")]
520 world,
521 })
522 }
523}
524
525impl FilesystemPackAssemblyClock {
526 fn document_time(self) -> DocumentTime {
527 match self {
528 Self::System => {
529 let timestamp = std::time::SystemTime::now()
530 .duration_since(std::time::UNIX_EPOCH)
531 .map_or(0, |duration| duration.as_secs() as i64);
532 DocumentTime::UnixTimestamp(timestamp)
533 }
534 Self::Fixed(document_time) => document_time,
535 }
536 }
537}
538
539fn resolve_and_create(
555 project: &ProjectSnapshot,
556 fonts: &crate::font_catalog::FontCatalog,
557 discovery: &DiscoverySpecification,
558 metadata: Option<&PackMetadata>,
559 authority: &FilesystemPackageAuthority,
560 packages: &ReadPackages,
561 disposition: PackageDisposition,
562) -> Result<(Pack, EcoVec<SourceDiagnostic>), CreationFailure> {
563 let mut attempted_specs: HashSet<String> = HashSet::new();
564 let mut package_failures = Vec::new();
565 let mut read_failures = PackageReadFailures::new();
566 let mut catalog = PackageCatalog::new();
567 loop {
568 let outcome = create(PackCreationInput {
569 project,
570 packages: &catalog,
571 fonts,
572 package_failures: &read_failures,
573 discovery,
574 metadata,
575 })
576 .map_err(|error| CreationFailure::Core {
577 error,
578 package_failures: std::mem::take(&mut package_failures),
579 })?;
580 match outcome {
581 PackCreationOutcome::Created { pack, warnings } => return Ok((pack, warnings)),
582 PackCreationOutcome::MissingPackageSpecifications(missing) => {
583 for spec in missing {
584 if !attempted_specs.insert(spec.to_string()) {
585 return Err(CreationFailure::Adapter(
590 FilesystemPackAssemblyError::Package {
591 message: "the representative creation compile did not accept the \
592 resolved package tree"
593 .to_owned(),
594 spec,
595 },
596 ));
597 }
598 match authority.read(&spec) {
599 Ok(read) => {
600 let (tree, _) = read.into_parts();
601 packages.record(spec.clone(), tree.clone());
602 read_failures.remove(&spec);
603 catalog
604 .insert(spec.clone(), tree, disposition)
605 .map_err(FilesystemPackAssemblyError::InvalidPackageCatalog)?;
606 }
607 Err(error) => {
608 read_failures.insert(error.failure().clone());
609 package_failures.push(error);
610 }
611 }
612 }
613 }
614 }
615 }
616}
617
618enum CreationFailure {
621 Core {
623 error: PackCreationError,
624 package_failures: Vec<FilesystemPackageAuthorityReadError>,
625 },
626 Adapter(FilesystemPackAssemblyError),
628}
629
630impl CreationFailure {
631 fn into_assembly_error(self, world: ReadWorld) -> FilesystemPackAssemblyError {
634 match self {
635 Self::Adapter(error) => error,
636 Self::Core {
637 error,
638 package_failures,
639 } => FilesystemPackAssemblyError::Creation(FilesystemPackAssemblyCreationError {
640 context: Box::new(PackAssemblyDiagnosticContext { world }),
641 error,
642 package_failures,
643 }),
644 }
645 }
646}
647
648impl From<FilesystemPackAssemblyError> for CreationFailure {
649 fn from(error: FilesystemPackAssemblyError) -> Self {
650 Self::Adapter(error)
651 }
652}
653
654pub struct PackAssemblyReport {
656 pack: Pack,
657 warnings: EcoVec<SourceDiagnostic>,
658 #[cfg(feature = "diagnostics")]
659 pub(crate) world: ReadWorld,
660}
661
662impl PackAssemblyReport {
663 pub fn pack(&self) -> &Pack {
665 &self.pack
666 }
667
668 pub fn warnings(&self) -> &[SourceDiagnostic] {
670 &self.warnings
671 }
672
673 pub fn into_parts(self) -> (Pack, EcoVec<SourceDiagnostic>) {
675 (self.pack, self.warnings)
676 }
677}
678
679#[derive(Debug)]
683pub struct PackAssemblyDiagnosticContext {
684 #[cfg_attr(not(feature = "diagnostics"), allow(dead_code))]
685 pub(crate) world: ReadWorld,
686}
687
688#[derive(Debug, thiserror::Error)]
690#[error("{error}")]
691pub struct FilesystemPackAssemblyCreationError {
692 context: Box<PackAssemblyDiagnosticContext>,
693 #[source]
694 error: PackCreationError,
695 package_failures: Vec<FilesystemPackageAuthorityReadError>,
696}
697
698impl FilesystemPackAssemblyCreationError {
699 pub fn context(&self) -> &PackAssemblyDiagnosticContext {
701 &self.context
702 }
703
704 pub fn error(&self) -> &PackCreationError {
706 &self.error
707 }
708
709 pub fn package_failures(&self) -> &[FilesystemPackageAuthorityReadError] {
711 &self.package_failures
712 }
713
714 pub fn into_parts(
716 self,
717 ) -> (
718 Box<PackAssemblyDiagnosticContext>,
719 PackCreationError,
720 Vec<FilesystemPackageAuthorityReadError>,
721 ) {
722 (self.context, self.error, self.package_failures)
723 }
724}
725
726#[derive(Debug, thiserror::Error)]
728#[error("invalid Discovery Specification: {source}")]
729pub struct FilesystemPackAssemblyDiscoveryError {
730 #[source]
731 source: crate::creation::DiscoverySpecificationError,
732}
733
734impl FilesystemPackAssemblyDiscoveryError {
735 pub fn source_error(&self) -> &crate::creation::DiscoverySpecificationError {
737 &self.source
738 }
739
740 pub fn into_source(self) -> crate::creation::DiscoverySpecificationError {
742 self.source
743 }
744}
745
746#[derive(Debug, thiserror::Error)]
748#[non_exhaustive]
749pub enum FilesystemPackAssemblyError {
750 #[error("{message}: {source}")]
751 Io {
752 message: String,
753 #[source]
754 source: std::io::Error,
755 },
756 #[error("`{0}` is outside the project root and cannot be packed")]
757 OutsideRoot(PathBuf),
758 #[error(transparent)]
759 Creation(FilesystemPackAssemblyCreationError),
760 #[error(transparent)]
761 DiscoverySpecification(FilesystemPackAssemblyDiscoveryError),
762 #[error("failed to write creation timings: {0}")]
763 Timings(String),
764 #[error("failed to load package {spec}: {message}")]
765 Package { spec: PackageSpec, message: String },
766 #[error(transparent)]
768 InvalidPackageCatalog(PackageCatalogError),
769 #[error(transparent)]
770 ProjectRead(#[from] fs_project::FilesystemProjectReadError),
771 #[error(transparent)]
772 FontRead(#[from] crate::fs_fonts::FilesystemFontReadError),
773}
774
775impl FilesystemPackAssemblyError {
776 pub(crate) fn io(message: &str, source: std::io::Error) -> Self {
777 Self::Io {
778 message: message.to_owned(),
779 source,
780 }
781 }
782}
783
784pub(crate) struct ReadWorld {
792 root: PathBuf,
793 #[cfg(feature = "diagnostics")]
794 workdir: Option<PathBuf>,
795 library: LazyHash<Library>,
796 main: FileId,
797 files: FileStore<ReadLoader>,
798 fonts: FontStore,
799}
800
801impl ReadWorld {
802 #[cfg(feature = "diagnostics")]
804 fn root(&self) -> &Path {
805 &self.root
806 }
807
808 #[cfg(feature = "diagnostics")]
809 fn workdir(&self) -> Option<&Path> {
810 self.workdir.as_deref()
811 }
812}
813
814impl fmt::Debug for ReadWorld {
815 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816 f.debug_struct("ReadWorld")
817 .field("root", &self.root)
818 .finish_non_exhaustive()
819 }
820}
821
822impl World for ReadWorld {
823 fn library(&self) -> &LazyHash<Library> {
824 &self.library
825 }
826
827 fn book(&self) -> &LazyHash<FontBook> {
829 self.fonts.book()
830 }
831
832 fn main(&self) -> FileId {
833 self.main
834 }
835
836 fn source(&self, id: FileId) -> FileResult<Source> {
837 self.files.source(id)
838 }
839
840 fn file(&self, id: FileId) -> FileResult<Bytes> {
841 self.files.file(id)
842 }
843
844 fn font(&self, _index: usize) -> Option<Font> {
845 None
846 }
847
848 fn today(&self, _offset: Option<Duration>) -> Option<Datetime> {
851 None
852 }
853}
854
855#[cfg(feature = "diagnostics")]
856impl typst_kit::diagnostics::DiagnosticWorld for ReadWorld {
857 fn name(&self, id: FileId) -> String {
858 match id.root() {
859 VirtualRoot::Project => id
860 .vpath()
861 .realize(self.root())
862 .ok()
863 .and_then(|path| relative_path(&path, self.workdir()?))
864 .map(|path| path.to_string_lossy().into_owned())
865 .unwrap_or_else(|| display_file_id(id)),
866 VirtualRoot::Package(_) => display_file_id(id),
867 }
868 }
869}
870
871#[cfg(feature = "diagnostics")]
872fn display_file_id(id: FileId) -> String {
873 match id.root() {
874 VirtualRoot::Project => id.vpath().get_without_slash().to_owned(),
875 VirtualRoot::Package(spec) => format!("{spec}{}", id.vpath().get_with_slash()),
876 }
877}
878
879#[cfg(feature = "diagnostics")]
880fn relative_path(path: &Path, base: &Path) -> Option<PathBuf> {
881 if path.is_absolute() != base.is_absolute() {
882 return path.is_absolute().then(|| path.to_path_buf());
883 }
884
885 let mut path_components = path.components();
886 let mut base_components = base.components();
887 let mut relative = Vec::new();
888 loop {
889 match (path_components.next(), base_components.next()) {
890 (None, None) => break,
891 (Some(component), None) => {
892 relative.push(component);
893 relative.extend(path_components.by_ref());
894 break;
895 }
896 (None, Some(_)) => relative.push(std::path::Component::ParentDir),
897 (Some(path), Some(base)) if relative.is_empty() && path == base => {}
898 (Some(path), Some(std::path::Component::CurDir)) => relative.push(path),
899 (Some(_), Some(std::path::Component::ParentDir)) => return None,
900 (Some(std::path::Component::Prefix(_) | std::path::Component::RootDir), Some(_))
901 | (Some(_), Some(std::path::Component::Prefix(_) | std::path::Component::RootDir)) => {
902 return path.is_absolute().then(|| path.to_path_buf());
903 }
904 (Some(path), Some(_)) => {
905 relative.push(std::path::Component::ParentDir);
906 relative.extend(base_components.map(|_| std::path::Component::ParentDir));
907 relative.push(path);
908 relative.extend(path_components.by_ref());
909 break;
910 }
911 }
912 }
913
914 Some(relative.iter().map(|part| part.as_os_str()).collect())
915}
916
917struct ReadLoader {
920 project: Arc<ProjectSnapshot>,
921 packages: Arc<ReadPackages>,
922}
923
924impl FileLoader for ReadLoader {
925 fn load(&self, id: FileId) -> FileResult<Bytes> {
926 let path = id.vpath().get_without_slash();
927 match id.root() {
928 VirtualRoot::Project => self.project.shared_file(path).map(|data| data.to_typst()),
929 VirtualRoot::Package(spec) => self.packages.file(spec, path),
930 }
931 .ok_or_else(|| FileError::NotFound(PathBuf::from(path)))
932 }
933}