tytanic_core/world_builder/
mod.rs1use 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
28pub trait ProvideFile: Send + Sync {
30 fn provide_source(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Source>;
35
36 fn provide_bytes(&self, id: FileId, progress: &mut dyn Progress) -> FileResult<Bytes>;
41
42 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#[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 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 pub fn project_provider(&self) -> &P {
89 &self.project
90 }
91
92 pub fn template_provider(&self) -> &T {
94 &self.template
95 }
96
97 pub fn spec(&self) -> &PackageSpec {
99 &self.spec
100 }
101
102 pub fn old(&self) -> Option<&PackageSpec> {
104 self.old.get()
105 }
106}
107
108impl<B, T> TemplateFileProviderShim<B, T> {
109 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
167pub trait ProvideFont: Send + Sync {
169 fn provide_font_book(&self) -> &LazyHash<FontBook>;
171
172 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
188pub trait ProvideLibrary: Send + Sync {
190 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
202pub trait ProvideDatetime: Send + Sync {
204 fn provide_today(&self, offset: Option<i64>) -> Option<Datetime>;
216
217 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
236pub 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 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 pub fn file_provider(self, value: &'w dyn ProvideFile) -> Self {
259 Self {
260 files: Some(value),
261 ..self
262 }
263 }
264
265 pub fn font_provider(self, value: &'w dyn ProvideFont) -> Self {
267 Self {
268 fonts: Some(value),
269 ..self
270 }
271 }
272
273 pub fn library_provider(self, value: &'w dyn ProvideLibrary) -> Self {
275 Self {
276 library: Some(value),
277 ..self
278 }
279 }
280
281 pub fn datetime_provider(self, value: &'w dyn ProvideDatetime) -> Self {
283 Self {
284 datetime: Some(value),
285 ..self
286 }
287 }
288
289 pub fn build(self, id: FileId) -> ComposedWorld<'w> {
293 self.try_build(id).unwrap()
294 }
295
296 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
316pub 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 pub fn builder() -> ComposedWorldBuilder<'w> {
329 ComposedWorldBuilder::new()
330 }
331}
332
333impl ComposedWorld<'_> {
334 pub fn reset(&self) {
336 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 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 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 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}