tytanic_core/world_builder/
mod.rs

1use std::sync::OnceLock;
2
3use typst::Library;
4use typst::World;
5use typst::diag::FileResult;
6use typst::foundations::Bytes;
7use typst::foundations::Datetime;
8use typst::syntax::FileId;
9use typst::syntax::Source;
10use typst::syntax::package::PackageSpec;
11use typst::text::Font;
12use typst::text::FontBook;
13use typst::utils::LazyHash;
14use typst_kit::download::Progress;
15use typst_kit::download::ProgressSink;
16
17pub mod datetime;
18pub mod file;
19pub mod font;
20pub mod library;
21
22macro_rules! forward_trait {
23    (impl<$pointee:ident> $trait:ident for [$($pointer:ty),+] $funcs:tt) => {
24        $(impl<$pointee: $trait> $trait for $pointer $funcs)+
25    };
26}
27
28/// A trait for providing access to files.
29pub trait ProvideFile: Send + Sync {
30    /// Provides a Typst source with the given file id.
31    ///
32    /// This may download a package, for which the progress callbacks will be
33    /// used.
34    fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source>;
35
36    /// Provides a generic file with the given file id.
37    ///
38    /// This may download a package, for which the progress callbacks will be
39    /// used.
40    fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes>;
41
42    /// Reset the cached files for the next compilation.
43    fn reset_all(&self);
44}
45
46forward_trait! {
47    impl<W> ProvideFile for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
48        fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source> {
49            W::provide_source(self, id, progress)
50        }
51
52        fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes> {
53            W::provide_bytes(self, id, progress)
54        }
55
56        fn reset_all(&self) {
57            W::reset_all(self)
58        }
59    }
60}
61
62/// A template file provider shim.
63///
64/// This provides template access through the preview import directly if the
65/// version matches and stores access to older versions for diagnostics.
66#[derive(Debug)]
67pub struct TemplateFileProviderShim<P, T> {
68    project: P,
69    template: T,
70    spec: PackageSpec,
71    old: OnceLock<PackageSpec>,
72}
73
74impl<P, T> TemplateFileProviderShim<P, T> {
75    /// Creates a new template file provider shim.
76    pub fn new(project: P, template: T, spec: PackageSpec) -> Self {
77        Self {
78            project,
79            template,
80            spec,
81            old: OnceLock::new(),
82        }
83    }
84}
85
86impl<P, T> TemplateFileProviderShim<P, T> {
87    /// The base provider used for all other files.
88    pub fn project_provider(&self) -> &P {
89        &self.project
90    }
91
92    /// The target provider used for files in the spec.
93    pub fn template_provider(&self) -> &T {
94        &self.template
95    }
96
97    /// The spec to re-route the imports for.
98    pub fn spec(&self) -> &PackageSpec {
99        &self.spec
100    }
101
102    /// The older spec that was accessed from this provider.
103    pub fn old(&self) -> Option<&PackageSpec> {
104        self.old.get()
105    }
106}
107
108impl<B, T> TemplateFileProviderShim<B, T> {
109    /// Record accesses to older versions of the current package spec.
110    fn record_access(&self, spec: &PackageSpec) {
111        if spec.namespace == self.spec.namespace
112            && spec.name == self.spec.name
113            && spec.version < self.spec.version
114        {
115            _ = self.old.set(spec.clone());
116        }
117    }
118}
119
120impl<B, T> ProvideFile for TemplateFileProviderShim<B, T>
121where
122    B: ProvideFile,
123    T: ProvideFile,
124{
125    fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source> {
126        let Some(spec) = id.package() else {
127            return self.template.provide_source(id, progress);
128        };
129
130        self.record_access(spec);
131
132        if spec.namespace == self.spec.namespace
133            && spec.name == self.spec.name
134            && spec.version == self.spec.version
135        {
136            let id = FileId::new(None, id.vpath().clone());
137            self.project.provide_source(id, progress)
138        } else {
139            self.template.provide_source(id, progress)
140        }
141    }
142
143    fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes> {
144        let Some(spec) = id.package() else {
145            return self.template.provide_bytes(id, progress);
146        };
147
148        self.record_access(spec);
149
150        if spec.namespace == self.spec.namespace
151            && spec.name == self.spec.name
152            && spec.version == self.spec.version
153        {
154            let id = FileId::new(None, id.vpath().clone());
155            self.project.provide_bytes(id, progress)
156        } else {
157            self.template.provide_bytes(id, progress)
158        }
159    }
160
161    fn reset_all(&self) {
162        self.project.reset_all();
163        self.template.reset_all();
164    }
165}
166
167/// A trait for providing access to fonts.
168pub trait ProvideFont: Send + Sync {
169    /// Provides the font book which stores metadata about fonts.
170    fn provide_font_book(&self) -> &LazyHash<FontBook>;
171
172    /// Provides a font with the given index.
173    fn provide_font(&self, index: usize) -> Option<Font>;
174}
175
176forward_trait! {
177    impl<W> ProvideFont for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
178        fn provide_font_book(&self) -> &LazyHash<FontBook> {
179            W::provide_font_book(self)
180        }
181
182        fn provide_font(&self, index: usize) -> Option<Font> {
183            W::provide_font(self, index)
184        }
185    }
186}
187
188/// A trait for providing access to libraries.
189pub trait ProvideLibrary: Send + Sync {
190    /// Provides the library.
191    fn provide_library(&self) -> &LazyHash<Library>;
192}
193
194forward_trait! {
195    impl<W> ProvideLibrary for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
196        fn provide_library(&self) -> &LazyHash<Library> {
197            W::provide_library(self)
198        }
199    }
200}
201
202/// A trait for providing access to date.
203pub trait ProvideDatetime: Send + Sync {
204    /// Provides the current date.
205    ///
206    /// If no offset is specified, the local date should be chosen. Otherwise,
207    /// the UTC date should be chosen with the corresponding offset in hours.
208    ///
209    /// If this function returns `None`, Typst's `datetime` function will
210    /// return an error.
211    ///
212    /// Note that most implementations should provide a date only or only very
213    /// course time increments to ensure Typst's incremental compilation cache
214    /// is not disrupted too much.
215    fn provide_today(&self, offset: Option<i64>) -> Option<Datetime>;
216
217    /// Reset the current date for the next compilation.
218    ///
219    /// Note that this is only relevant for those providers which actually
220    /// provide the current date.
221    fn reset_today(&self);
222}
223
224forward_trait! {
225    impl<W> ProvideDatetime for [std::boxed::Box<W>, std::sync::Arc<W>, &W] {
226        fn provide_today(&self, offset: Option<i64>) -> Option<Datetime> {
227            W::provide_today(self, offset)
228        }
229
230        fn reset_today(&self) {
231            W::reset_today(self)
232        }
233    }
234}
235
236/// A builder for [`ComposedWorld`].
237pub struct ComposedWorldBuilder<'w> {
238    files: Option<&'w dyn ProvideFile>,
239    fonts: Option<&'w dyn ProvideFont>,
240    library: Option<&'w dyn ProvideLibrary>,
241    datetime: Option<&'w dyn ProvideDatetime>,
242}
243
244impl ComposedWorldBuilder<'_> {
245    /// Creates a new builder.
246    pub fn new() -> Self {
247        Self {
248            files: None,
249            fonts: None,
250            library: None,
251            datetime: None,
252        }
253    }
254}
255
256impl<'w> ComposedWorldBuilder<'w> {
257    /// Configure the file provider.
258    pub fn file_provider(self, value: &'w dyn ProvideFile) -> Self {
259        Self {
260            files: Some(value),
261            ..self
262        }
263    }
264
265    /// Configure the font provider.
266    pub fn font_provider(self, value: &'w dyn ProvideFont) -> Self {
267        Self {
268            fonts: Some(value),
269            ..self
270        }
271    }
272
273    /// Configure the library provider.
274    pub fn library_provider(self, value: &'w dyn ProvideLibrary) -> Self {
275        Self {
276            library: Some(value),
277            ..self
278        }
279    }
280
281    /// Configure the datetime provider.
282    pub fn datetime_provider(self, value: &'w dyn ProvideDatetime) -> Self {
283        Self {
284            datetime: Some(value),
285            ..self
286        }
287    }
288
289    /// Build the world with the configured providers.
290    ///
291    /// Panics if a provider is missing.
292    pub fn build(self, id: FileId) -> ComposedWorld<'w> {
293        self.try_build(id).unwrap()
294    }
295
296    /// Build the world with the configured providers.
297    ///
298    /// Returns `None` if a provider is missing.
299    pub fn try_build(self, id: FileId) -> Option<ComposedWorld<'w>> {
300        Some(ComposedWorld {
301            files: self.files?,
302            fonts: self.fonts?,
303            library: self.library?,
304            datetime: self.datetime?,
305            id,
306        })
307    }
308}
309
310impl Default for ComposedWorldBuilder<'_> {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316/// A shim around the various provider traits which together implement a whole
317/// [`World`].
318pub struct ComposedWorld<'w> {
319    files: &'w dyn ProvideFile,
320    fonts: &'w dyn ProvideFont,
321    library: &'w dyn ProvideLibrary,
322    datetime: &'w dyn ProvideDatetime,
323    id: FileId,
324}
325
326impl<'w> ComposedWorld<'w> {
327    /// Creates a new builder.
328    pub fn builder() -> ComposedWorldBuilder<'w> {
329        ComposedWorldBuilder::new()
330    }
331}
332
333impl ComposedWorld<'_> {
334    /// Resets the inner providers for the next compilation.
335    pub fn reset(&self) {
336        // TODO(tinger): We probably really want exclusive access here, no
337        // provider should be used while it's being reset.
338        self.files.reset_all();
339        self.datetime.reset_today();
340    }
341}
342
343impl World for ComposedWorld<'_> {
344    fn library(&self) -> &LazyHash<Library> {
345        self.library.provide_library()
346    }
347
348    fn book(&self) -> &LazyHash<FontBook> {
349        self.fonts.provide_font_book()
350    }
351
352    fn main(&self) -> FileId {
353        self.id
354    }
355
356    fn source(&self, id: FileId) -> FileResult<Source> {
357        self.files.provide_source(id, &mut ProgressSink)
358    }
359
360    fn file(&self, id: FileId) -> FileResult<Bytes> {
361        self.files.provide_bytes(id, &mut ProgressSink)
362    }
363
364    fn font(&self, index: usize) -> Option<Font> {
365        self.fonts.provide_font(index)
366    }
367
368    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
369        self.datetime.provide_today(offset)
370    }
371}
372
373#[cfg(test)]
374#[allow(dead_code)]
375pub(crate) mod test_utils {
376    use std::collections::HashMap;
377    use std::sync::LazyLock;
378
379    use chrono::DateTime;
380    use datetime::FixedDateProvider;
381    use file::VirtualFileProvider;
382    use font::VirtualFontProvider;
383    use library::LibraryProvider;
384
385    use super::file::VirtualFileSlot;
386    use super::*;
387    use crate::library::augmented_default_library;
388
389    pub(crate) fn test_file_provider(source: Source) -> VirtualFileProvider {
390        let mut map = HashMap::new();
391        map.insert(source.id(), VirtualFileSlot::from_source(source.clone()));
392
393        VirtualFileProvider::from_slots(map)
394    }
395
396    pub(crate) static TEST_FONT_PROVIDER: LazyLock<VirtualFontProvider> = LazyLock::new(|| {
397        let fonts: Vec<_> = typst_assets::fonts()
398            .flat_map(|data| Font::iter(Bytes::new(data)))
399            .collect();
400
401        let book = FontBook::from_fonts(&fonts);
402        VirtualFontProvider::new(book, fonts)
403    });
404
405    pub(crate) static TEST_DEFAULT_LIBRARY_PROVIDER: LazyLock<LibraryProvider> =
406        LazyLock::new(LibraryProvider::new);
407
408    pub(crate) static TEST_AUGMENTED_LIBRARY_PROVIDER: LazyLock<LibraryProvider> =
409        LazyLock::new(|| LibraryProvider::with_library(augmented_default_library()));
410
411    pub(crate) static TEST_DATETIME_PROVIDER: LazyLock<FixedDateProvider> =
412        LazyLock::new(|| FixedDateProvider::new(DateTime::from_timestamp(0, 0).unwrap()));
413
414    pub(crate) fn virtual_world<'w>(
415        source: Source,
416        files: &'w mut VirtualFileProvider,
417        library: &'w LibraryProvider,
418    ) -> ComposedWorld<'w> {
419        files
420            .slots_mut()
421            .insert(source.id(), VirtualFileSlot::from_source(source.clone()));
422
423        ComposedWorld::builder()
424            .file_provider(files)
425            .font_provider(&*TEST_FONT_PROVIDER)
426            .library_provider(library)
427            .datetime_provider(&*TEST_DATETIME_PROVIDER)
428            .build(source.id())
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use std::collections::HashMap;
435
436    use typst::syntax::VirtualPath;
437    use typst::syntax::package::PackageVersion;
438
439    use super::*;
440    use crate::world_builder::file::VirtualFileProvider;
441    use crate::world_builder::file::VirtualFileSlot;
442
443    #[test]
444    fn test_template_file_provider_shim() {
445        let spec = PackageSpec {
446            namespace: "preview".into(),
447            name: "self".into(),
448            version: PackageVersion {
449                major: 0,
450                minor: 1,
451                patch: 0,
452            },
453        };
454
455        let project_lib_id = FileId::new(None, VirtualPath::new("lib.typ"));
456        let template_lib_id = FileId::new(None, VirtualPath::new("lib.typ"));
457        let template_main_id = FileId::new(None, VirtualPath::new("main.typ"));
458
459        let project_lib = Source::new(project_lib_id, "#let foo(bar) = bar".into());
460        let template_lib = Source::new(template_lib_id, "#let bar = [qux]".into());
461        let template_main = Source::new(
462            template_main_id,
463            "#import \"@preview/self:0.1.0\"\n#show foo".into(),
464        );
465
466        let mut project = HashMap::new();
467        let mut template = HashMap::new();
468
469        project.insert(
470            project_lib.id(),
471            VirtualFileSlot::from_source(project_lib.clone()),
472        );
473        template.insert(
474            template_lib.id(),
475            VirtualFileSlot::from_source(template_lib.clone()),
476        );
477        template.insert(
478            template_main.id(),
479            VirtualFileSlot::from_source(template_main.clone()),
480        );
481
482        let project = VirtualFileProvider::from_slots(project);
483        let template = VirtualFileProvider::from_slots(template);
484
485        let shim = TemplateFileProviderShim::new(project, template, spec.clone());
486
487        // lib.typ is available inside the template
488        assert_eq!(
489            shim.provide_source(
490                FileId::new(None, VirtualPath::new("lib.typ")),
491                &mut ProgressSink
492            )
493            .unwrap()
494            .text(),
495            template_lib.text()
496        );
497
498        // main.typ is available inside the template
499        assert_eq!(
500            shim.provide_source(
501                FileId::new(None, VirtualPath::new("main.typ")),
502                &mut ProgressSink
503            )
504            .unwrap()
505            .text(),
506            template_main.text()
507        );
508
509        // lib.typ is also available from the project
510        assert_eq!(
511            shim.provide_source(
512                FileId::new(Some(spec), VirtualPath::new("lib.typ")),
513                &mut ProgressSink
514            )
515            .unwrap()
516            .text(),
517            project_lib.text()
518        );
519    }
520}