Skip to main content

typst_as_lib/
lib.rs

1#![warn(missing_docs)]
2//! Small wrapper around [Typst](https://github.com/typst/typst) that makes it easier to use it as a templating engine.
3//!
4//! See the [repository README](https://github.com/Relacibo/typst-as-lib) for usage examples.
5//! Inspired by <https://github.com/tfachmann/typst-as-library/blob/main/src/lib.rs>
6use std::borrow::Cow;
7use std::ops::Deref;
8use std::path::PathBuf;
9
10use cached_file_resolver::IntoCachedFileResolver;
11use chrono::{DateTime, Datelike, Duration, Utc};
12use conversions::{IntoBytes, IntoFileId, IntoFonts, IntoSource};
13use ecow::EcoVec;
14use file_resolver::{
15    FileResolver, FileSystemResolver, MainSourceFileResolver, StaticFileResolver,
16    StaticSourceFileResolver,
17};
18use thiserror::Error;
19use typst::diag::{FileError, FileResult, HintedString, SourceDiagnostic, Warned};
20use typst::foundations::{Bytes, Datetime, Dict, Module, Scope, Value};
21use typst::syntax::{FileId, Source};
22use typst::text::{Font, FontBook};
23use typst::utils::LazyHash;
24use typst::{Document, Library, LibraryExt};
25use util::not_found;
26
27/// Caching wrapper for file resolvers.
28pub mod cached_file_resolver;
29/// Type conversion traits for Typst types.
30pub mod conversions;
31/// File resolution for Typst sources and binaries.
32pub mod file_resolver;
33pub(crate) mod util;
34
35#[cfg(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
36/// Package resolution and downloading from the Typst package repository.
37pub mod package_resolver;
38
39#[cfg(feature = "typst-kit-fonts")]
40/// Configuration options for `typst-kit` font searching.
41pub mod typst_kit_options;
42
43/// Main entry point for compiling Typst documents.
44///
45/// Use [`TypstEngine::builder()`] to construct an instance. You can optionally set a
46/// main file with [`main_file()`](TypstTemplateEngineBuilder::main_file), which allows
47/// compiling without specifying the file ID each time.
48///
49/// # Examples
50///
51/// With main file (compile without file ID):
52///
53/// ```rust,no_run
54/// # use typst_as_lib::TypstEngine;
55/// # use typst::layout::PagedDocument;
56/// static TEMPLATE: &str = "Hello World!";
57/// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
58///
59/// let engine = TypstEngine::builder()
60///     .main_file(TEMPLATE)
61///     .fonts([FONT])
62///     .build();
63///
64/// // Compile the main file directly
65/// let doc: PagedDocument = engine.compile().output.expect("Compilation failed");
66/// ```
67///
68/// Without main file (must provide file ID):
69///
70/// ```rust,no_run
71/// # use typst_as_lib::TypstEngine;
72/// # use typst::layout::PagedDocument;
73/// static TEMPLATE: &str = "Hello World!";
74/// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
75///
76/// let engine = TypstEngine::builder()
77///     .fonts([FONT])
78///     .with_static_source_file_resolver([("template.typ", TEMPLATE)])
79///     .build();
80///
81/// // Must specify file ID for each compile
82/// let doc: PagedDocument = engine.compile("template.typ").output.expect("Compilation failed");
83/// ```
84///
85/// See also: [Examples directory](https://github.com/Relacibo/typst-as-lib/tree/main/examples)
86pub struct TypstEngine<T = TypstTemplateCollection> {
87    template: T,
88    book: LazyHash<FontBook>,
89    inject_location: Option<InjectLocation>,
90    file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
91    library: LazyHash<Library>,
92    comemo_evict_max_age: Option<usize>,
93    fonts: Vec<FontEnum>,
94}
95
96/// Type state indicating no main file is set.
97#[derive(Debug, Clone, Copy)]
98pub struct TypstTemplateCollection;
99
100/// Type state indicating a main file has been set.
101#[derive(Debug, Clone, Copy)]
102pub struct TypstTemplateMainFile {
103    source_id: FileId,
104}
105
106impl<T> TypstEngine<T> {
107    fn do_compile<Doc>(
108        &self,
109        main_source_id: FileId,
110        inputs: Option<Dict>,
111    ) -> Warned<Result<Doc, TypstAsLibError>>
112    where
113        Doc: Document,
114    {
115        let library = if let Some(inputs) = inputs {
116            let lib = self.create_injected_library(inputs);
117            match lib {
118                Ok(lib) => Cow::Owned(lib),
119                Err(err) => {
120                    return Warned {
121                        output: Err(err),
122                        warnings: Default::default(),
123                    };
124                }
125            }
126        } else {
127            Cow::Borrowed(&self.library)
128        };
129        let world = TypstWorld {
130            main_source_id,
131            library,
132            now: Utc::now(),
133            file_resolvers: &self.file_resolvers,
134            book: &self.book,
135            fonts: &self.fonts,
136        };
137        let Warned { output, warnings } = typst::compile(&world);
138
139        if let Some(comemo_evict_max_age) = self.comemo_evict_max_age {
140            comemo::evict(comemo_evict_max_age);
141        }
142
143        Warned {
144            output: output.map_err(Into::into),
145            warnings,
146        }
147    }
148
149    fn create_injected_library<D>(&self, input: D) -> Result<LazyHash<Library>, TypstAsLibError>
150    where
151        D: Into<Dict>,
152    {
153        let Self {
154            inject_location,
155            library,
156            ..
157        } = self;
158        let mut lib = library.deref().clone();
159        let (module_name, value_name) = if let Some(InjectLocation {
160            module_name,
161            value_name,
162        }) = inject_location
163        {
164            (*module_name, *value_name)
165        } else {
166            ("sys", "inputs")
167        };
168        {
169            let global = lib.global.scope_mut();
170            let input_dict: Dict = input.into();
171            if let Some(module_value) = global.get_mut(module_name) {
172                let module_value = module_value.write()?;
173                if let Value::Module(module) = module_value {
174                    let scope = module.scope_mut();
175                    if let Some(target) = scope.get_mut(value_name) {
176                        // Override existing field
177                        *target.write()? = Value::Dict(input_dict);
178                    } else {
179                        // Write new field into existing module scope
180                        scope.define(value_name, input_dict);
181                    }
182                } else {
183                    // Override existing non module value
184                    let mut scope = Scope::deduplicating();
185                    scope.define(value_name, input_dict);
186                    let module = Module::new(module_name, scope);
187                    *module_value = Value::Module(module);
188                }
189            } else {
190                // Create new module and field
191                let mut scope = Scope::deduplicating();
192                scope.define(value_name, input_dict);
193                let module = Module::new(module_name, scope);
194                global.define(module_name, module);
195            }
196        }
197        Ok(LazyHash::new(lib))
198    }
199}
200
201impl TypstEngine<TypstTemplateCollection> {
202    /// Creates a new builder for configuring a [`TypstEngine`].
203    ///
204    /// # Example
205    ///
206    /// ```rust,no_run
207    /// # use typst_as_lib::TypstEngine;
208    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
209    ///
210    /// let engine = TypstEngine::builder()
211    ///     .fonts([FONT])
212    ///     .build();
213    /// ```
214    pub fn builder() -> TypstTemplateEngineBuilder {
215        TypstTemplateEngineBuilder::default()
216    }
217}
218
219impl TypstEngine<TypstTemplateCollection> {
220    /// Compiles a Typst document with input data injected as `sys.inputs`.
221    ///
222    /// The input will be available in Typst scripts via `#import sys: inputs`.
223    ///
224    /// To change the injection location, use [`custom_inject_location()`](TypstTemplateEngineBuilder::custom_inject_location).
225    ///
226    /// # Example
227    ///
228    /// ```rust,no_run
229    /// # use typst_as_lib::TypstEngine;
230    /// # use typst::foundations::{Dict, IntoValue};
231    /// # use typst::layout::PagedDocument;
232    /// static TEMPLATE: &str = "#import sys: inputs\n#inputs.name";
233    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
234    ///
235    /// let engine = TypstEngine::builder()
236    ///     .fonts([FONT])
237    ///     .with_static_source_file_resolver([("main.typ", TEMPLATE)])
238    ///     .build();
239    ///
240    /// let mut inputs = Dict::new();
241    /// inputs.insert("name".into(), "World".into_value());
242    ///
243    /// let doc: PagedDocument = engine.compile_with_input("main.typ", inputs)
244    ///     .output
245    ///     .expect("Compilation failed");
246    /// ```
247    ///
248    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
249    pub fn compile_with_input<F, D, Doc>(
250        &self,
251        main_source_id: F,
252        inputs: D,
253    ) -> Warned<Result<Doc, TypstAsLibError>>
254    where
255        F: IntoFileId,
256        D: Into<Dict>,
257        Doc: Document,
258    {
259        self.do_compile(main_source_id.into_file_id(), Some(inputs.into()))
260    }
261
262    /// Compiles a Typst document without input data.
263    pub fn compile<F, Doc>(&self, main_source_id: F) -> Warned<Result<Doc, TypstAsLibError>>
264    where
265        F: IntoFileId,
266        Doc: Document,
267    {
268        self.do_compile(main_source_id.into_file_id(), None)
269    }
270}
271
272impl TypstEngine<TypstTemplateMainFile> {
273    /// Compiles the main file with input data injected as `sys.inputs`.
274    ///
275    /// The input will be available in Typst scripts via `#import sys: inputs`.
276    ///
277    /// To change the injection location, use [`custom_inject_location()`](TypstTemplateEngineBuilder::custom_inject_location).
278    ///
279    /// # Example
280    ///
281    /// ```rust,no_run
282    /// # use typst_as_lib::TypstEngine;
283    /// # use typst::foundations::{Dict, IntoValue};
284    /// # use typst::layout::PagedDocument;
285    /// static TEMPLATE: &str = "#import sys: inputs\nHello #inputs.name!";
286    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
287    ///
288    /// let engine = TypstEngine::builder()
289    ///     .main_file(TEMPLATE)
290    ///     .fonts([FONT])
291    ///     .build();
292    ///
293    /// let mut inputs = Dict::new();
294    /// inputs.insert("name".into(), "World".into_value());
295    ///
296    /// let doc: PagedDocument = engine.compile_with_input(inputs)
297    ///     .output
298    ///     .expect("Compilation failed");
299    /// ```
300    ///
301    /// See also: [small_example.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/small_example.rs)
302    pub fn compile_with_input<D, Doc>(&self, inputs: D) -> Warned<Result<Doc, TypstAsLibError>>
303    where
304        D: Into<Dict>,
305        Doc: Document,
306    {
307        let TypstTemplateMainFile { source_id } = self.template;
308        self.do_compile(source_id, Some(inputs.into()))
309    }
310
311    /// Compiles the main file without input data.
312    pub fn compile<Doc>(&self) -> Warned<Result<Doc, TypstAsLibError>>
313    where
314        Doc: Document,
315    {
316        let TypstTemplateMainFile { source_id } = self.template;
317        self.do_compile(source_id, None)
318    }
319}
320
321/// Builder for constructing a [`TypstEngine`].
322pub struct TypstTemplateEngineBuilder<T = TypstTemplateCollection> {
323    template: T,
324    inject_location: Option<InjectLocation>,
325    file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
326    comemo_evict_max_age: Option<usize>,
327    fonts: Option<Vec<Font>>,
328    #[cfg(feature = "typst-kit-fonts")]
329    typst_kit_font_options: Option<typst_kit_options::TypstKitFontOptions>,
330}
331
332impl Default for TypstTemplateEngineBuilder {
333    fn default() -> Self {
334        Self {
335            template: TypstTemplateCollection,
336            inject_location: Default::default(),
337            file_resolvers: Default::default(),
338            comemo_evict_max_age: Some(0),
339            fonts: Default::default(),
340            #[cfg(feature = "typst-kit-fonts")]
341            typst_kit_font_options: None,
342        }
343    }
344}
345
346impl TypstTemplateEngineBuilder<TypstTemplateCollection> {
347    /// Sets the main file for compilation.
348    ///
349    /// This is optional. If not set, you must provide a file ID on each compile call.
350    ///
351    /// # Example
352    ///
353    /// ```rust,no_run
354    /// # use typst_as_lib::TypstEngine;
355    /// # use typst::layout::PagedDocument;
356    /// static TEMPLATE: &str = "Hello World!";
357    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
358    ///
359    /// let engine = TypstEngine::builder()
360    ///     .main_file(TEMPLATE)
361    ///     .fonts([FONT])
362    ///     .build();
363    ///
364    /// let doc: PagedDocument = engine.compile().output.expect("Compilation failed");
365    /// ```
366    ///
367    /// See also: [small_example.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/small_example.rs)
368    pub fn main_file<S: IntoSource>(
369        self,
370        source: S,
371    ) -> TypstTemplateEngineBuilder<TypstTemplateMainFile> {
372        let source = source.into_source();
373        let source_id = source.id();
374        let template = TypstTemplateMainFile { source_id };
375        let TypstTemplateEngineBuilder {
376            inject_location,
377            mut file_resolvers,
378            comemo_evict_max_age,
379            fonts,
380            #[cfg(feature = "typst-kit-fonts")]
381            typst_kit_font_options,
382            ..
383        } = self;
384        file_resolvers.push(Box::new(MainSourceFileResolver::new(source)));
385        TypstTemplateEngineBuilder {
386            template,
387            inject_location,
388            file_resolvers,
389            comemo_evict_max_age,
390            fonts,
391            #[cfg(feature = "typst-kit-fonts")]
392            typst_kit_font_options,
393        }
394    }
395}
396
397impl<T> TypstTemplateEngineBuilder<T> {
398    /// Customizes where input data is injected in the Typst environment.
399    ///
400    /// By default, inputs are available as `sys.inputs`.
401    pub fn custom_inject_location(
402        mut self,
403        module_name: &'static str,
404        value_name: &'static str,
405    ) -> Self {
406        self.inject_location = Some(InjectLocation {
407            module_name,
408            value_name,
409        });
410        self
411    }
412
413    /// Adds fonts for rendering.
414    ///
415    /// Accepts font data as `&[u8]`, `Vec<u8>`, `Bytes`, or `Font`.
416    ///
417    /// For automatic system font discovery, see `typst-kit-fonts` feature.
418    ///
419    /// # Example
420    ///
421    /// ```rust,no_run
422    /// # use typst_as_lib::TypstEngine;
423    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
424    ///
425    /// let engine = TypstEngine::builder()
426    ///     .fonts([FONT])
427    ///     .build();
428    /// ```
429    pub fn fonts<I, F>(mut self, fonts: I) -> Self
430    where
431        I: IntoIterator<Item = F>,
432        F: IntoFonts,
433    {
434        let fonts = fonts
435            .into_iter()
436            .flat_map(IntoFonts::into_fonts)
437            .collect::<Vec<_>>();
438        self.fonts = Some(fonts);
439        self
440    }
441
442    /// Enables system font discovery using `typst-kit`.
443    ///
444    /// See [`typst_kit_options::TypstKitFontOptions`] for configuration.
445    ///
446    /// # Example
447    ///
448    /// ```rust,no_run
449    /// # use typst_as_lib::TypstEngine;
450    /// # use typst_as_lib::typst_kit_options::TypstKitFontOptions;
451    /// let engine = TypstEngine::builder()
452    ///     .search_fonts_with(TypstKitFontOptions::default())
453    ///     .build();
454    /// ```
455    ///
456    /// See also: [font_searcher.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/font_searcher.rs)
457    #[cfg(feature = "typst-kit-fonts")]
458    pub fn search_fonts_with(mut self, options: typst_kit_options::TypstKitFontOptions) -> Self {
459        self.typst_kit_font_options = Some(options);
460        self
461    }
462
463    /// Adds a custom file resolver.
464    ///
465    /// Resolvers are tried in order until one successfully resolves the file.
466    pub fn add_file_resolver<F>(mut self, file_resolver: F) -> Self
467    where
468        F: FileResolver + Send + Sync + 'static,
469    {
470        self.file_resolvers.push(Box::new(file_resolver));
471        self
472    }
473
474    /// Adds static source files embedded in memory.
475    ///
476    /// Accepts sources as `&str`, `String`, `(&str, &str)` (path, content),
477    /// `(FileId, &str)`, or `Source`.
478    ///
479    /// # Example
480    ///
481    /// ```rust,no_run
482    /// # use typst_as_lib::TypstEngine;
483    /// # use typst::layout::PagedDocument;
484    /// static MAIN: &str = "#import \"lib.typ\": greet\n#greet()";
485    /// static LIB: &str = "#let greet() = [Hello World!]";
486    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
487    ///
488    /// let engine = TypstEngine::builder()
489    ///     .fonts([FONT])
490    ///     .with_static_source_file_resolver([
491    ///         ("main.typ", MAIN),
492    ///         ("lib.typ", LIB),
493    ///     ])
494    ///     .build();
495    ///
496    /// let doc: PagedDocument = engine.compile("main.typ").output.expect("Compilation failed");
497    /// ```
498    ///
499    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
500    pub fn with_static_source_file_resolver<IS, S>(self, sources: IS) -> Self
501    where
502        IS: IntoIterator<Item = S>,
503        S: IntoSource,
504    {
505        self.add_file_resolver(StaticSourceFileResolver::new(sources))
506    }
507
508    /// Adds static binary files embedded in memory (e.g., images).
509    ///
510    /// # Example
511    ///
512    /// ```rust,no_run
513    /// # use typst_as_lib::TypstEngine;
514    /// static TEMPLATE: &str = r#"#image("logo.png")"#;
515    /// static LOGO: &[u8] = include_bytes!("../examples/templates/images/typst.png");
516    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
517    ///
518    /// let engine = TypstEngine::builder()
519    ///     .main_file(TEMPLATE)
520    ///     .fonts([FONT])
521    ///     .with_static_file_resolver([("logo.png", LOGO)])
522    ///     .build();
523    /// ```
524    ///
525    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
526    pub fn with_static_file_resolver<IB, F, B>(self, binaries: IB) -> Self
527    where
528        IB: IntoIterator<Item = (F, B)>,
529        F: IntoFileId,
530        B: IntoBytes,
531    {
532        self.add_file_resolver(StaticFileResolver::new(binaries))
533    }
534
535    /// Enables loading files from the file system.
536    ///
537    /// Files are resolved relative to `root`. Files outside of `root` cannot be accessed.
538    ///
539    /// # Example
540    ///
541    /// ```rust,no_run
542    /// # use typst_as_lib::TypstEngine;
543    /// static TEMPLATE: &str = r#"#include "header.typ""#;
544    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
545    ///
546    /// let engine = TypstEngine::builder()
547    ///     .main_file(TEMPLATE)
548    ///     .fonts([FONT])
549    ///     .with_file_system_resolver("./templates")
550    ///     .build();
551    /// ```
552    ///
553    /// See also: [resolve_packages.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_packages.rs)
554    pub fn with_file_system_resolver<P>(self, root: P) -> Self
555    where
556        P: Into<PathBuf>,
557    {
558        self.add_file_resolver(FileSystemResolver::new(root.into()).into_cached())
559    }
560
561    /// Sets the maximum age for comemo cache eviction after compilation.
562    ///
563    /// Default is `Some(0)`, which evicts after each compilation.
564    pub fn comemo_evict_max_age(&mut self, comemo_evict_max_age: Option<usize>) -> &mut Self {
565        self.comemo_evict_max_age = comemo_evict_max_age;
566        self
567    }
568
569    /// Enables downloading packages from the Typst package repository.
570    ///
571    /// Packages are cached on the file system for reuse.
572    ///
573    /// # Example
574    ///
575    /// ```rust,no_run
576    /// # use typst_as_lib::TypstEngine;
577    /// static TEMPLATE: &str = r#"#import "@preview/example:0.1.0": *"#;
578    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
579    ///
580    /// let engine = TypstEngine::builder()
581    ///     .main_file(TEMPLATE)
582    ///     .fonts([FONT])
583    ///     .with_package_file_resolver()
584    ///     .build();
585    /// ```
586    ///
587    /// See also: [resolve_packages.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_packages.rs)
588    #[cfg(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
589    pub fn with_package_file_resolver(self) -> Self {
590        use package_resolver::PackageResolver;
591        let file_resolver = PackageResolver::builder()
592            .with_file_system_cache()
593            .build()
594            .into_cached();
595        self.add_file_resolver(file_resolver)
596    }
597
598    /// Builds the [`TypstEngine`] with the configured options.
599    pub fn build(self) -> TypstEngine<T> {
600        let TypstTemplateEngineBuilder {
601            template,
602            inject_location,
603            file_resolvers,
604            comemo_evict_max_age,
605            fonts,
606            #[cfg(feature = "typst-kit-fonts")]
607            typst_kit_font_options,
608        } = self;
609
610        let mut book = FontBook::new();
611        if let Some(fonts) = &fonts {
612            for f in fonts {
613                book.push(f.info().clone());
614            }
615        }
616
617        #[allow(unused_mut)]
618        let mut fonts: Vec<_> = fonts.into_iter().flatten().map(FontEnum::Font).collect();
619
620        #[cfg(feature = "typst-kit-fonts")]
621        if let Some(typst_kit_font_options) = typst_kit_font_options {
622            let typst_kit_options::TypstKitFontOptions {
623                include_system_fonts,
624                include_dirs,
625                #[cfg(feature = "typst-kit-embed-fonts")]
626                include_embedded_fonts,
627            } = typst_kit_font_options;
628            let mut searcher = typst_kit::fonts::Fonts::searcher();
629            #[cfg(feature = "typst-kit-embed-fonts")]
630            searcher.include_embedded_fonts(include_embedded_fonts);
631            let typst_kit::fonts::Fonts {
632                book: typst_kit_book,
633                fonts: typst_kit_fonts,
634            } = searcher
635                .include_system_fonts(include_system_fonts)
636                .search_with(include_dirs);
637            let len = typst_kit_fonts.len();
638            let font_slots = typst_kit_fonts.into_iter().map(FontEnum::FontSlot);
639            if fonts.is_empty() {
640                book = typst_kit_book;
641                fonts = font_slots.collect();
642            } else {
643                for i in 0..len {
644                    let Some(info) = typst_kit_book.info(i) else {
645                        break;
646                    };
647                    book.push(info.clone());
648                }
649                fonts.extend(font_slots);
650            }
651        }
652
653        #[cfg(not(feature = "typst-html"))]
654        let library = typst::Library::builder().build();
655
656        #[cfg(feature = "typst-html")]
657        let library = typst::Library::builder()
658            .with_features([typst::Feature::Html].into_iter().collect())
659            .build();
660
661        TypstEngine {
662            template,
663            inject_location,
664            file_resolvers,
665            comemo_evict_max_age,
666            library: LazyHash::new(library),
667            book: LazyHash::new(book),
668            fonts,
669        }
670    }
671}
672
673struct TypstWorld<'a> {
674    library: Cow<'a, LazyHash<Library>>,
675    main_source_id: FileId,
676    now: DateTime<Utc>,
677    book: &'a LazyHash<FontBook>,
678    file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
679    fonts: &'a [FontEnum],
680}
681
682impl typst::World for TypstWorld<'_> {
683    fn library(&self) -> &LazyHash<Library> {
684        self.library.as_ref()
685    }
686
687    fn book(&self) -> &LazyHash<FontBook> {
688        self.book
689    }
690
691    fn main(&self) -> FileId {
692        self.main_source_id
693    }
694
695    fn source(&self, id: FileId) -> FileResult<Source> {
696        let Self { file_resolvers, .. } = *self;
697        let mut last_error = not_found(id);
698        for file_resolver in file_resolvers {
699            match file_resolver.resolve_source(id) {
700                Ok(source) => return Ok(source.into_owned()),
701                Err(error) => last_error = error,
702            }
703        }
704        Err(last_error)
705    }
706
707    fn file(&self, id: FileId) -> FileResult<Bytes> {
708        let Self { file_resolvers, .. } = *self;
709        let mut last_error = not_found(id);
710        for file_resolver in file_resolvers {
711            match file_resolver.resolve_binary(id) {
712                Ok(file) => return Ok(file.into_owned()),
713                Err(error) => last_error = error,
714            }
715        }
716        Err(last_error)
717    }
718
719    fn font(&self, id: usize) -> Option<Font> {
720        self.fonts[id].get()
721    }
722
723    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
724        let mut now = self.now;
725        if let Some(offset) = offset {
726            now += Duration::hours(offset);
727        }
728        let date = now.date_naive();
729        let year = date.year();
730        let month = (date.month0() + 1) as u8;
731        let day = (date.day0() + 1) as u8;
732        Datetime::from_ymd(year, month, day)
733    }
734}
735
736#[derive(Debug, Clone)]
737struct InjectLocation {
738    module_name: &'static str,
739    value_name: &'static str,
740}
741
742/// Errors that can occur when using typst-as-lib.
743#[derive(Debug, Clone, Error)]
744pub enum TypstAsLibError {
745    /// Errors from Typst source compilation.
746    #[error("Typst source error: {0:?}")]
747    TypstSource(EcoVec<SourceDiagnostic>),
748    /// Errors from file operations.
749    #[error("Typst file error: {0}")]
750    TypstFile(#[from] FileError),
751    /// The specified main source file was not found.
752    #[error("Source file does not exist in collection: {0:?}")]
753    MainSourceFileDoesNotExist(FileId),
754    /// Errors with additional hints from Typst.
755    #[error("Typst hinted String: {0:?}")]
756    HintedString(HintedString),
757    /// Other unspecified errors.
758    #[error("Unspecified: {0}!")]
759    Unspecified(ecow::EcoString),
760}
761
762impl From<HintedString> for TypstAsLibError {
763    fn from(value: HintedString) -> Self {
764        TypstAsLibError::HintedString(value)
765    }
766}
767
768impl From<ecow::EcoString> for TypstAsLibError {
769    fn from(value: ecow::EcoString) -> Self {
770        TypstAsLibError::Unspecified(value)
771    }
772}
773
774impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
775    fn from(value: EcoVec<SourceDiagnostic>) -> Self {
776        TypstAsLibError::TypstSource(value)
777    }
778}
779
780/// Wrapper for different font types.
781#[derive(Debug)]
782pub enum FontEnum {
783    /// A directly loaded font.
784    Font(Font),
785    /// A lazy font slot from typst-kit.
786    #[cfg(feature = "typst-kit-fonts")]
787    FontSlot(typst_kit::fonts::FontSlot),
788}
789
790impl FontEnum {
791    /// Retrieves the font, loading it if necessary.
792    pub fn get(&self) -> Option<Font> {
793        match self {
794            FontEnum::Font(font) => Some(font.clone()),
795            #[cfg(feature = "typst-kit-fonts")]
796            FontEnum::FontSlot(font_slot) => font_slot.get(),
797        }
798    }
799}