tytanic_core/world_builder/
mod.rs

1use typst::Library;
2use typst::World;
3use typst::diag::FileResult;
4use typst::foundations::Bytes;
5use typst::foundations::Datetime;
6use typst::syntax::FileId;
7use typst::syntax::Source;
8use typst::text::Font;
9use typst::text::FontBook;
10use typst::utils::LazyHash;
11use typst_kit::download::Progress;
12use typst_kit::download::ProgressSink;
13
14pub mod datetime;
15pub mod file;
16pub mod font;
17pub mod library;
18
19macro_rules! forward_trait {
20    (impl<$pointee:ident> $trait:ident for [$($pointer:ty),+] $funcs:tt) => {
21        $(impl<$pointee: $trait> $trait for $pointer $funcs)+
22    };
23}
24
25/// A trait for providing access to files.
26pub trait ProvideFile: Send + Sync {
27    /// Provides a Typst source with the given file id.
28    ///
29    /// This may download a package, for which the progress callbacks will be
30    /// used.
31    fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source>;
32
33    /// Provides a generic file with the given file id.
34    ///
35    /// This may download a package, for which the progress callbacks will be
36    /// used.
37    fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes>;
38
39    /// Reset the cached files for the next compilation.
40    fn reset_all(&self);
41}
42
43forward_trait! {
44    impl<W> ProvideFile for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
45        fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source> {
46            W::provide_source(self, id, progress)
47        }
48
49        fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes> {
50            W::provide_bytes(self, id, progress)
51        }
52
53        fn reset_all(&self) {
54            W::reset_all(self)
55        }
56    }
57}
58
59/// A trait for providing access to fonts.
60pub trait ProvideFont: Send + Sync {
61    /// Provides the font book which stores metadata about fonts.
62    fn provide_font_book(&self) -> &LazyHash<FontBook>;
63
64    /// Provides a font with the given index.
65    fn provide_font(&self, index: usize) -> Option<Font>;
66}
67
68forward_trait! {
69    impl<W> ProvideFont for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
70        fn provide_font_book(&self) -> &LazyHash<FontBook> {
71            W::provide_font_book(self)
72        }
73
74        fn provide_font(&self, index: usize) -> Option<Font> {
75            W::provide_font(self, index)
76        }
77    }
78}
79
80/// A trait for providing access to libraries.
81pub trait ProvideLibrary: Send + Sync {
82    /// Provides the library.
83    fn provide_library(&self) -> &LazyHash<Library>;
84}
85
86forward_trait! {
87    impl<W> ProvideLibrary for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
88        fn provide_library(&self) -> &LazyHash<Library> {
89            W::provide_library(self)
90        }
91    }
92}
93
94/// A trait for providing access to date.
95pub trait ProvideDatetime: Send + Sync {
96    /// Provides the current date.
97    ///
98    /// If no offset is specified, the local date should be chosen. Otherwise,
99    /// the UTC date should be chosen with the corresponding offset in hours.
100    ///
101    /// If this function returns `None`, Typst's `datetime` function will
102    /// return an error.
103    ///
104    /// Note that most implementations should provide a date only or only very
105    /// course time increments to ensure Typst's incremental compilation cache
106    /// is not disrupted too much.
107    fn provide_today(&self, offset: Option<i64>) -> Option<Datetime>;
108
109    /// Reset the current date for the next compilation.
110    ///
111    /// Note that this is only relevant for those providers which actually
112    /// provide the current date.
113    fn reset_today(&self);
114}
115
116forward_trait! {
117    impl<W> ProvideDatetime for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
118        fn provide_today(&self, offset: Option<i64>) -> Option<Datetime> {
119            W::provide_today(self, offset)
120        }
121
122        fn reset_today(&self) {
123            W::reset_today(self)
124        }
125    }
126}
127
128/// A builder for [`ComposedWorld`].
129pub struct ComposedWorldBuilder<'w> {
130    files: Option<&'w dyn ProvideFile>,
131    fonts: Option<&'w dyn ProvideFont>,
132    library: Option<&'w dyn ProvideLibrary>,
133    datetime: Option<&'w dyn ProvideDatetime>,
134}
135
136impl ComposedWorldBuilder<'_> {
137    /// Creates a new builder.
138    pub fn new() -> Self {
139        Self {
140            files: None,
141            fonts: None,
142            library: None,
143            datetime: None,
144        }
145    }
146}
147
148impl<'w> ComposedWorldBuilder<'w> {
149    /// Configure the file provider.
150    pub fn file_provider(self, value: &'w dyn ProvideFile) -> Self {
151        Self {
152            files: Some(value),
153            ..self
154        }
155    }
156
157    /// Configure the font provider.
158    pub fn font_provider(self, value: &'w dyn ProvideFont) -> Self {
159        Self {
160            fonts: Some(value),
161            ..self
162        }
163    }
164
165    /// Configure the library provider.
166    pub fn library_provider(self, value: &'w dyn ProvideLibrary) -> Self {
167        Self {
168            library: Some(value),
169            ..self
170        }
171    }
172
173    /// Configure the datetime provider.
174    pub fn datetime_provider(self, value: &'w dyn ProvideDatetime) -> Self {
175        Self {
176            datetime: Some(value),
177            ..self
178        }
179    }
180
181    /// Build the world with the configured providers.
182    ///
183    /// Panics if a provider is missing.
184    pub fn build(self, id: FileId) -> ComposedWorld<'w> {
185        self.try_build(id).unwrap()
186    }
187
188    /// Build the world with the configured providers.
189    ///
190    /// Returns `None` if a provider is missing.
191    pub fn try_build(self, id: FileId) -> Option<ComposedWorld<'w>> {
192        Some(ComposedWorld {
193            files: self.files?,
194            fonts: self.fonts?,
195            library: self.library?,
196            datetime: self.datetime?,
197            id,
198        })
199    }
200}
201
202impl Default for ComposedWorldBuilder<'_> {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208/// A shim around the various provider traits which together implement a whole
209/// [`World`].
210pub struct ComposedWorld<'w> {
211    files: &'w dyn ProvideFile,
212    fonts: &'w dyn ProvideFont,
213    library: &'w dyn ProvideLibrary,
214    datetime: &'w dyn ProvideDatetime,
215    id: FileId,
216}
217
218impl<'w> ComposedWorld<'w> {
219    /// Creates a new builder.
220    pub fn builder() -> ComposedWorldBuilder<'w> {
221        ComposedWorldBuilder::new()
222    }
223}
224
225impl ComposedWorld<'_> {
226    /// Resets the inner providers for the next compilation.
227    pub fn reset(&self) {
228        // TODO(tinger): We probably really want exclusive access here, no
229        // provider should be used while it's being reset.
230        self.files.reset_all();
231        self.datetime.reset_today();
232    }
233}
234
235impl World for ComposedWorld<'_> {
236    fn library(&self) -> &LazyHash<Library> {
237        self.library.provide_library()
238    }
239
240    fn book(&self) -> &LazyHash<FontBook> {
241        self.fonts.provide_font_book()
242    }
243
244    fn main(&self) -> FileId {
245        self.id
246    }
247
248    fn source(&self, id: FileId) -> FileResult<Source> {
249        self.files.provide_source(id, &mut ProgressSink)
250    }
251
252    fn file(&self, id: FileId) -> FileResult<Bytes> {
253        self.files.provide_bytes(id, &mut ProgressSink)
254    }
255
256    fn font(&self, index: usize) -> Option<Font> {
257        self.fonts.provide_font(index)
258    }
259
260    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
261        self.datetime.provide_today(offset)
262    }
263}
264
265#[cfg(test)]
266#[allow(dead_code)]
267pub(crate) mod test_utils {
268    use std::collections::HashMap;
269    use std::sync::LazyLock;
270
271    use chrono::DateTime;
272    use datetime::FixedDateProvider;
273    use file::VirtualFileProvider;
274    use font::VirtualFontProvider;
275    use library::LibraryProvider;
276
277    use super::file::VirtualFileSlot;
278    use super::*;
279    use crate::library::augmented_default_library;
280
281    pub(crate) fn test_file_provider(source: Source) -> VirtualFileProvider {
282        let mut map = HashMap::new();
283        map.insert(source.id(), VirtualFileSlot::from_source(source.clone()));
284
285        VirtualFileProvider::from_slots(map)
286    }
287
288    pub(crate) static TEST_FONT_PROVIDER: LazyLock<VirtualFontProvider> = LazyLock::new(|| {
289        let fonts: Vec<_> = typst_assets::fonts()
290            .flat_map(|data| Font::iter(Bytes::new(data)))
291            .collect();
292
293        let book = FontBook::from_fonts(&fonts);
294        VirtualFontProvider::new(book, fonts)
295    });
296
297    pub(crate) static TEST_DEFAULT_LIBRARY_PROVIDER: LazyLock<LibraryProvider> =
298        LazyLock::new(LibraryProvider::new);
299
300    pub(crate) static TEST_AUGMENTED_LIBRARY_PROVIDER: LazyLock<LibraryProvider> =
301        LazyLock::new(|| LibraryProvider::with_library(augmented_default_library()));
302
303    pub(crate) static TEST_DATETIME_PROVIDER: LazyLock<FixedDateProvider> =
304        LazyLock::new(|| FixedDateProvider::new(DateTime::from_timestamp(0, 0).unwrap()));
305
306    pub(crate) fn virtual_world<'w>(
307        source: Source,
308        files: &'w mut VirtualFileProvider,
309        library: &'w LibraryProvider,
310    ) -> ComposedWorld<'w> {
311        files
312            .slots_mut()
313            .insert(source.id(), VirtualFileSlot::from_source(source.clone()));
314
315        ComposedWorld::builder()
316            .file_provider(files)
317            .font_provider(&*TEST_FONT_PROVIDER)
318            .library_provider(library)
319            .datetime_provider(&*TEST_DATETIME_PROVIDER)
320            .build(source.id())
321    }
322}