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