Skip to main content

typst_as_lib/
file_resolver.rs

1use ecow::eco_format;
2use std::{
3    borrow::Cow,
4    collections::HashMap,
5    path::{Path, PathBuf},
6};
7use typst::{
8    diag::{FileError, FileResult},
9    foundations::Bytes,
10    syntax::{FileId, Source},
11};
12
13use crate::{
14    cached_file_resolver::{CachedFileResolver, IntoCachedFileResolver},
15    conversions::{IntoBytes, IntoFileId, IntoSource},
16    util::{bytes_to_source, not_found},
17};
18
19// https://github.com/typst/typst/blob/16736feb13eec87eb9ca114deaeb4f7eeb7409d2/crates/typst-kit/src/package.rs#L18
20/// The default packages sub directory within the package and package cache paths.
21pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
22
23/// Trait for resolving Typst files from various sources.
24pub trait FileResolver {
25    /// Resolves a binary file (e.g., images, fonts).
26    fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>>;
27    /// Resolves a source file (e.g., `.typ` files).
28    fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>>;
29}
30
31#[derive(Debug, Clone)]
32pub(crate) struct MainSourceFileResolver {
33    main_source: Source,
34}
35
36impl MainSourceFileResolver {
37    pub(crate) fn new(main_source: Source) -> Self {
38        Self { main_source }
39    }
40}
41
42impl FileResolver for MainSourceFileResolver {
43    fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
44        Err(not_found(id))
45    }
46
47    fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
48        let Self { main_source } = self;
49        if id == main_source.id() {
50            return Ok(Cow::Borrowed(main_source));
51        }
52        Err(not_found(id))
53    }
54}
55
56/// Resolves source files from a static in-memory map.
57#[derive(Debug, Clone)]
58pub struct StaticSourceFileResolver {
59    sources: HashMap<FileId, Source>,
60}
61
62impl StaticSourceFileResolver {
63    pub(crate) fn new<IS, S>(sources: IS) -> Self
64    where
65        IS: IntoIterator<Item = S>,
66        S: IntoSource,
67    {
68        let sources = sources
69            .into_iter()
70            .map(|s| {
71                let s = s.into_source();
72                (s.id(), s)
73            })
74            .collect();
75        Self { sources }
76    }
77}
78
79impl FileResolver for StaticSourceFileResolver {
80    fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
81        Err(not_found(id))
82    }
83
84    fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
85        self.sources
86            .get(&id)
87            .map(Cow::Borrowed)
88            .ok_or_else(|| not_found(id))
89    }
90}
91
92/// Resolves binary files from a static in-memory map.
93#[derive(Debug, Clone)]
94pub struct StaticFileResolver {
95    binaries: HashMap<FileId, Bytes>,
96}
97
98impl StaticFileResolver {
99    pub(crate) fn new<IB, F, B>(binaries: IB) -> Self
100    where
101        IB: IntoIterator<Item = (F, B)>,
102        F: IntoFileId,
103        B: IntoBytes,
104    {
105        let binaries = binaries
106            .into_iter()
107            .map(|(id, b)| (id.into_file_id(), b.into_bytes()))
108            .collect();
109        Self { binaries }
110    }
111}
112
113impl FileResolver for StaticFileResolver {
114    fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
115        self.binaries
116            .get(&id)
117            .map(Cow::Borrowed)
118            .ok_or_else(|| not_found(id))
119    }
120
121    fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
122        Err(not_found(id))
123    }
124}
125
126/// Resolves files from the file system.
127///
128/// Files are resolved relative to `root` and cannot escape it.
129#[derive(Debug, Clone)]
130pub struct FileSystemResolver {
131    root: PathBuf,
132    local_package_root: Option<PathBuf>,
133}
134
135impl FileSystemResolver {
136    /// Creates a new file system resolver with the given root directory.
137    ///
138    /// # Example
139    ///
140    /// ```rust,no_run
141    /// # use typst_as_lib::file_resolver::FileSystemResolver;
142    /// let resolver = FileSystemResolver::new("./templates".into());
143    /// ```
144    pub fn new(root: PathBuf) -> Self {
145        let mut root = root.clone();
146        // trailing slash is necessary for resolve function, which is, what this 'hack' does
147        // https://users.rust-lang.org/t/trailing-in-paths/43166/9
148        root.push("");
149        Self {
150            root,
151            local_package_root: None,
152        }
153    }
154
155    /// Sets a custom path for local packages.
156    #[deprecated(
157        since = "0.14.1",
158        note = "Use `FileSystemResolver::local_package_root` instead"
159    )]
160    pub fn with_local_package_root(self, path: PathBuf) -> Self {
161        Self {
162            local_package_root: Some(path),
163            ..self
164        }
165    }
166
167    /// Sets a custom path for local packages.
168    ///
169    /// # Example
170    ///
171    /// ```rust,no_run
172    /// # use typst_as_lib::file_resolver::FileSystemResolver;
173    /// let resolver = FileSystemResolver::new("./templates".into())
174    ///     .local_package_root("./local-packages".into());
175    /// ```
176    pub fn local_package_root(self, local_package_root: PathBuf) -> Self {
177        Self {
178            local_package_root: Some(local_package_root),
179            ..self
180        }
181    }
182
183    fn resolve_bytes(&self, id: FileId) -> FileResult<Vec<u8>> {
184        let Self {
185            root,
186            local_package_root,
187        } = self;
188        // https://github.com/typst/typst/blob/16736feb13eec87eb9ca114deaeb4f7eeb7409d2/crates/typst-kit/src/package.rs#L102C16-L102C38
189        let dir: Cow<Path> = if let Some(package) = id.package() {
190            let data_dir = if let Some(data_dir) = local_package_root {
191                Cow::Borrowed(data_dir)
192            } else if let Some(data_dir) = dirs::data_dir() {
193                Cow::Owned(data_dir.join(DEFAULT_PACKAGES_SUBDIR))
194            } else {
195                return Err(FileError::Other(Some(eco_format!("No data dir set!"))));
196            };
197            let subdir = Path::new(package.namespace.as_str())
198                .join(package.name.as_str())
199                .join(package.version.to_string());
200            Cow::Owned(data_dir.join(subdir))
201        } else {
202            Cow::Borrowed(root)
203        };
204
205        let path = id
206            .vpath()
207            .resolve(&dir)
208            .ok_or_else(|| FileError::NotFound(dir.to_path_buf()))?;
209        let content = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
210        Ok(content)
211    }
212}
213
214impl IntoCachedFileResolver for FileSystemResolver {
215    fn into_cached(self) -> CachedFileResolver<Self> {
216        CachedFileResolver::new(self)
217            .with_in_memory_source_cache()
218            .with_in_memory_binary_cache()
219    }
220}
221
222impl FileResolver for FileSystemResolver {
223    fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
224        let b = self.resolve_bytes(id)?;
225        Ok(Cow::Owned(Bytes::new(b)))
226    }
227
228    fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
229        let file = self.resolve_bytes(id)?;
230        let source = bytes_to_source(id, &file)?;
231        Ok(Cow::Owned(source))
232    }
233}