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 mut builder = TypstWorldBuilder::new(self, main_source_id);
117        if let Some(inputs) = inputs {
118            builder = builder.with_inputs(inputs);
119        }
120        let world = match builder.build() {
121            Ok(world) => world,
122            Err(err) => {
123                return Warned {
124                    output: Err(err),
125                    warnings: Default::default(),
126                };
127            }
128        };
129        let Warned { output, warnings } = typst::compile(&world);
130        if let Some(max_age) = self.comemo_evict_max_age {
131            comemo::evict(max_age);
132        }
133        Warned {
134            output: output.map_err(Into::into),
135            warnings,
136        }
137    }
138
139    fn create_injected_library<D>(&self, input: D) -> Result<LazyHash<Library>, TypstAsLibError>
140    where
141        D: Into<Dict>,
142    {
143        let Self {
144            inject_location,
145            library,
146            ..
147        } = self;
148        let mut lib = library.deref().clone();
149        let (module_name, value_name) = if let Some(InjectLocation {
150            module_name,
151            value_name,
152        }) = inject_location
153        {
154            (*module_name, *value_name)
155        } else {
156            ("sys", "inputs")
157        };
158        {
159            let global = lib.global.scope_mut();
160            let input_dict: Dict = input.into();
161            if let Some(module_value) = global.get_mut(module_name) {
162                let module_value = module_value.write()?;
163                if let Value::Module(module) = module_value {
164                    let scope = module.scope_mut();
165                    if let Some(target) = scope.get_mut(value_name) {
166                        // Override existing field
167                        *target.write()? = Value::Dict(input_dict);
168                    } else {
169                        // Write new field into existing module scope
170                        scope.define(value_name, input_dict);
171                    }
172                } else {
173                    // Override existing non module value
174                    let mut scope = Scope::deduplicating();
175                    scope.define(value_name, input_dict);
176                    let module = Module::new(module_name, scope);
177                    *module_value = Value::Module(module);
178                }
179            } else {
180                // Create new module and field
181                let mut scope = Scope::deduplicating();
182                scope.define(value_name, input_dict);
183                let module = Module::new(module_name, scope);
184                global.define(module_name, module);
185            }
186        }
187        Ok(LazyHash::new(lib))
188    }
189}
190
191impl TypstEngine<TypstTemplateCollection> {
192    /// Creates a new builder for configuring a [`TypstEngine`].
193    ///
194    /// # Example
195    ///
196    /// ```rust,no_run
197    /// # use typst_as_lib::TypstEngine;
198    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
199    ///
200    /// let engine = TypstEngine::builder()
201    ///     .fonts([FONT])
202    ///     .build();
203    /// ```
204    pub fn builder() -> TypstTemplateEngineBuilder {
205        TypstTemplateEngineBuilder::default()
206    }
207}
208
209impl TypstEngine<TypstTemplateCollection> {
210    /// Compiles a Typst document with input data injected as `sys.inputs`.
211    ///
212    /// The input will be available in Typst scripts via `#import sys: inputs`.
213    ///
214    /// To change the injection location, use [`custom_inject_location()`](TypstTemplateEngineBuilder::custom_inject_location).
215    ///
216    /// # Example
217    ///
218    /// ```rust,no_run
219    /// # use typst_as_lib::TypstEngine;
220    /// # use typst::foundations::{Dict, IntoValue};
221    /// # use typst::layout::PagedDocument;
222    /// static TEMPLATE: &str = "#import sys: inputs\n#inputs.name";
223    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
224    ///
225    /// let engine = TypstEngine::builder()
226    ///     .fonts([FONT])
227    ///     .with_static_source_file_resolver([("main.typ", TEMPLATE)])
228    ///     .build();
229    ///
230    /// let mut inputs = Dict::new();
231    /// inputs.insert("name".into(), "World".into_value());
232    ///
233    /// let doc: PagedDocument = engine.compile_with_input("main.typ", inputs)
234    ///     .output
235    ///     .expect("Compilation failed");
236    /// ```
237    ///
238    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
239    pub fn compile_with_input<F, D, Doc>(
240        &self,
241        main_source_id: F,
242        inputs: D,
243    ) -> Warned<Result<Doc, TypstAsLibError>>
244    where
245        F: IntoFileId,
246        D: Into<Dict>,
247        Doc: Document,
248    {
249        self.do_compile(main_source_id.into_file_id(), Some(inputs.into()))
250    }
251
252    /// Compiles a Typst document without input data.
253    pub fn compile<F, Doc>(&self, main_source_id: F) -> Warned<Result<Doc, TypstAsLibError>>
254    where
255        F: IntoFileId,
256        Doc: Document,
257    {
258        self.do_compile(main_source_id.into_file_id(), None)
259    }
260
261    /// Returns a [`TypstWorldBuilder`] for constructing a [`TypstWorld`] bound to a specific file.
262    ///
263    /// This is an advanced low-level API. The caller is responsible for driving compilation
264    /// (e.g. via `typst::compile`) and for managing the `comemo` cache afterwards.
265    /// No cache eviction is performed automatically — use [`with_world`](Self::with_world)
266    /// if you want eviction handled for you.
267    ///
268    /// # Example
269    /// ```rust,ignore
270    /// # use typst_as_lib::TypstEngine;
271    /// let engine = TypstEngine::builder().build();
272    ///
273    /// let world = engine.world_builder("/main.typ")
274    ///     .with_inputs(my_inputs)
275    ///     .build()?;
276    /// let doc = typst::compile(&world).output.expect("Failed");
277    /// comemo::evict(30); // caller manages cache eviction
278    /// ```
279    pub fn world_builder<I>(&self, main_source_id: I) -> TypstWorldBuilder<'_, TypstTemplateCollection>
280    where
281        I: IntoFileId,
282    {
283        TypstWorldBuilder::new(self, main_source_id.into_file_id())
284    }
285
286    /// Execute a closure with a [`TypstWorld`] for a specific file,
287    /// optionally injecting custom inputs.
288    ///
289    /// Runs the `comemo` cache eviction after the closure returns.
290    /// For full control use [`world_builder`](Self::world_builder) instead.
291    ///
292    /// # Example
293    /// ```rust,ignore
294    /// # use typst_as_lib::TypstEngine;
295    /// let engine = TypstEngine::builder().build();
296    ///
297    /// let pdf_bytes = engine.with_world("/main.typ", |world| {
298    ///     let doc = typst::compile(world).output.expect("Failed");
299    ///     typst_pdf::pdf(&doc, Default::default()).expect("Failed")
300    /// }).unwrap();
301    ///
302    /// // With inputs:
303    /// let pdf_bytes = engine.with_world("/main.typ", |world| { ... }).unwrap();
304    /// ```
305    pub fn with_world<F, I, R>(
306        &self,
307        main_source_id: I,
308        f: F,
309    ) -> Result<R, TypstAsLibError>
310    where
311        I: IntoFileId,
312        F: FnOnce(&TypstWorld<'_>) -> R,
313    {
314        let world = self.world_builder(main_source_id).build()?;
315        let result = f(&world);
316        if let Some(max_age) = self.comemo_evict_max_age {
317            comemo::evict(max_age);
318        }
319        Ok(result)
320    }
321}
322
323impl TypstEngine<TypstTemplateMainFile> {
324    /// Compiles the main file with input data injected as `sys.inputs`.
325    ///
326    /// The input will be available in Typst scripts via `#import sys: inputs`.
327    ///
328    /// To change the injection location, use [`custom_inject_location()`](TypstTemplateEngineBuilder::custom_inject_location).
329    ///
330    /// # Example
331    ///
332    /// ```rust,no_run
333    /// # use typst_as_lib::TypstEngine;
334    /// # use typst::foundations::{Dict, IntoValue};
335    /// # use typst::layout::PagedDocument;
336    /// static TEMPLATE: &str = "#import sys: inputs\nHello #inputs.name!";
337    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
338    ///
339    /// let engine = TypstEngine::builder()
340    ///     .main_file(TEMPLATE)
341    ///     .fonts([FONT])
342    ///     .build();
343    ///
344    /// let mut inputs = Dict::new();
345    /// inputs.insert("name".into(), "World".into_value());
346    ///
347    /// let doc: PagedDocument = engine.compile_with_input(inputs)
348    ///     .output
349    ///     .expect("Compilation failed");
350    /// ```
351    ///
352    /// See also: [small_example.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/small_example.rs)
353    pub fn compile_with_input<D, Doc>(&self, inputs: D) -> Warned<Result<Doc, TypstAsLibError>>
354    where
355        D: Into<Dict>,
356        Doc: Document,
357    {
358        let TypstTemplateMainFile { source_id } = self.template;
359        self.do_compile(source_id, Some(inputs.into()))
360    }
361
362    /// Compiles the main file without input data.
363    pub fn compile<Doc>(&self) -> Warned<Result<Doc, TypstAsLibError>>
364    where
365        Doc: Document,
366    {
367        let TypstTemplateMainFile { source_id } = self.template;
368        self.do_compile(source_id, None)
369    }
370
371    /// Returns a [`TypstWorldBuilder`] using the engine's pre-configured main file.
372    ///
373    /// This is an advanced low-level API. The caller is responsible for driving compilation
374    /// (e.g. via `typst::compile`) and for managing the `comemo` cache afterwards.
375    /// No cache eviction is performed automatically — use [`with_world`](Self::with_world)
376    /// if you want eviction handled for you.
377    ///
378    /// # Example
379    /// ```rust,ignore
380    /// # use typst_as_lib::TypstEngine;
381    /// let engine = TypstEngine::builder().main_file("= Hello").build();
382    ///
383    /// let world = engine.world_builder()
384    ///     .with_inputs(my_inputs)
385    ///     .build()?;
386    /// let doc = typst::compile(&world).output.expect("Failed");
387    /// comemo::evict(30); // caller manages cache eviction
388    /// ```
389    pub fn world_builder(&self) -> TypstWorldBuilder<'_, TypstTemplateMainFile> {
390        let TypstTemplateMainFile { source_id } = self.template;
391        TypstWorldBuilder::new(self, source_id)
392    }
393
394    /// Execute a closure with a [`TypstWorld`] using the engine's pre-configured main file,
395    /// optionally injecting custom inputs.
396    ///
397    /// Runs the `comemo` cache eviction after the closure returns.
398    /// For full control use [`world_builder`](Self::world_builder) instead.
399    ///
400    /// # Example
401    /// ```rust,ignore
402    /// # use typst_as_lib::TypstEngine;
403    /// let engine = TypstEngine::builder().main_file("= Hello").build();
404    ///
405    /// let pdf_bytes = engine.with_world(|world| {
406    ///     let doc = typst::compile(world).output.expect("Failed");
407    ///     typst_pdf::pdf(&doc, Default::default()).expect("Failed")
408    /// }).unwrap();
409    /// ```
410    pub fn with_world<F, R>(&self, f: F) -> Result<R, TypstAsLibError>
411    where
412        F: FnOnce(&TypstWorld<'_>) -> R,
413    {
414        let world = self.world_builder().build()?;
415        let result = f(&world);
416        if let Some(max_age) = self.comemo_evict_max_age {
417            comemo::evict(max_age);
418        }
419        Ok(result)
420    }
421}
422
423/// Builder for constructing a [`TypstEngine`].
424pub struct TypstTemplateEngineBuilder<T = TypstTemplateCollection> {
425    template: T,
426    inject_location: Option<InjectLocation>,
427    file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
428    comemo_evict_max_age: Option<usize>,
429    fonts: Option<Vec<Font>>,
430    #[cfg(feature = "typst-kit-fonts")]
431    typst_kit_font_options: Option<typst_kit_options::TypstKitFontOptions>,
432}
433
434impl Default for TypstTemplateEngineBuilder {
435    fn default() -> Self {
436        Self {
437            template: TypstTemplateCollection,
438            inject_location: Default::default(),
439            file_resolvers: Default::default(),
440            comemo_evict_max_age: Some(0),
441            fonts: Default::default(),
442            #[cfg(feature = "typst-kit-fonts")]
443            typst_kit_font_options: None,
444        }
445    }
446}
447
448impl TypstTemplateEngineBuilder<TypstTemplateCollection> {
449    /// Sets the main file for compilation.
450    ///
451    /// This is optional. If not set, you must provide a file ID on each compile call.
452    ///
453    /// # Example
454    ///
455    /// ```rust,no_run
456    /// # use typst_as_lib::TypstEngine;
457    /// # use typst::layout::PagedDocument;
458    /// static TEMPLATE: &str = "Hello World!";
459    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
460    ///
461    /// let engine = TypstEngine::builder()
462    ///     .main_file(TEMPLATE)
463    ///     .fonts([FONT])
464    ///     .build();
465    ///
466    /// let doc: PagedDocument = engine.compile().output.expect("Compilation failed");
467    /// ```
468    ///
469    /// See also: [small_example.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/small_example.rs)
470    pub fn main_file<S: IntoSource>(
471        self,
472        source: S,
473    ) -> TypstTemplateEngineBuilder<TypstTemplateMainFile> {
474        let source = source.into_source();
475        let source_id = source.id();
476        let template = TypstTemplateMainFile { source_id };
477        let TypstTemplateEngineBuilder {
478            inject_location,
479            mut file_resolvers,
480            comemo_evict_max_age,
481            fonts,
482            #[cfg(feature = "typst-kit-fonts")]
483            typst_kit_font_options,
484            ..
485        } = self;
486        file_resolvers.push(Box::new(MainSourceFileResolver::new(source)));
487        TypstTemplateEngineBuilder {
488            template,
489            inject_location,
490            file_resolvers,
491            comemo_evict_max_age,
492            fonts,
493            #[cfg(feature = "typst-kit-fonts")]
494            typst_kit_font_options,
495        }
496    }
497}
498
499impl<T> TypstTemplateEngineBuilder<T> {
500    /// Customizes where input data is injected in the Typst environment.
501    ///
502    /// By default, inputs are available as `sys.inputs`.
503    pub fn custom_inject_location(
504        mut self,
505        module_name: &'static str,
506        value_name: &'static str,
507    ) -> Self {
508        self.inject_location = Some(InjectLocation {
509            module_name,
510            value_name,
511        });
512        self
513    }
514
515    /// Adds fonts for rendering.
516    ///
517    /// Accepts font data as `&[u8]`, `Vec<u8>`, `Bytes`, or `Font`.
518    ///
519    /// For automatic system font discovery, see `typst-kit-fonts` feature.
520    ///
521    /// # Example
522    ///
523    /// ```rust,no_run
524    /// # use typst_as_lib::TypstEngine;
525    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
526    ///
527    /// let engine = TypstEngine::builder()
528    ///     .fonts([FONT])
529    ///     .build();
530    /// ```
531    pub fn fonts<I, F>(mut self, fonts: I) -> Self
532    where
533        I: IntoIterator<Item = F>,
534        F: IntoFonts,
535    {
536        let fonts = fonts
537            .into_iter()
538            .flat_map(IntoFonts::into_fonts)
539            .collect::<Vec<_>>();
540        self.fonts = Some(fonts);
541        self
542    }
543
544    /// Enables system font discovery using `typst-kit`.
545    ///
546    /// See [`typst_kit_options::TypstKitFontOptions`] for configuration.
547    ///
548    /// # Example
549    ///
550    /// ```rust,no_run
551    /// # use typst_as_lib::TypstEngine;
552    /// # use typst_as_lib::typst_kit_options::TypstKitFontOptions;
553    /// let engine = TypstEngine::builder()
554    ///     .search_fonts_with(TypstKitFontOptions::default())
555    ///     .build();
556    /// ```
557    ///
558    /// See also: [font_searcher.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/font_searcher.rs)
559    #[cfg(feature = "typst-kit-fonts")]
560    pub fn search_fonts_with(mut self, options: typst_kit_options::TypstKitFontOptions) -> Self {
561        self.typst_kit_font_options = Some(options);
562        self
563    }
564
565    /// Adds a custom file resolver.
566    ///
567    /// Resolvers are tried in order until one successfully resolves the file.
568    pub fn add_file_resolver<F>(mut self, file_resolver: F) -> Self
569    where
570        F: FileResolver + Send + Sync + 'static,
571    {
572        self.file_resolvers.push(Box::new(file_resolver));
573        self
574    }
575
576    /// Adds static source files embedded in memory.
577    ///
578    /// Accepts sources as `&str`, `String`, `(&str, &str)` (path, content),
579    /// `(FileId, &str)`, or `Source`.
580    ///
581    /// # Example
582    ///
583    /// ```rust,no_run
584    /// # use typst_as_lib::TypstEngine;
585    /// # use typst::layout::PagedDocument;
586    /// static MAIN: &str = "#import \"lib.typ\": greet\n#greet()";
587    /// static LIB: &str = "#let greet() = [Hello World!]";
588    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
589    ///
590    /// let engine = TypstEngine::builder()
591    ///     .fonts([FONT])
592    ///     .with_static_source_file_resolver([
593    ///         ("main.typ", MAIN),
594    ///         ("lib.typ", LIB),
595    ///     ])
596    ///     .build();
597    ///
598    /// let doc: PagedDocument = engine.compile("main.typ").output.expect("Compilation failed");
599    /// ```
600    ///
601    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
602    pub fn with_static_source_file_resolver<IS, S>(self, sources: IS) -> Self
603    where
604        IS: IntoIterator<Item = S>,
605        S: IntoSource,
606    {
607        self.add_file_resolver(StaticSourceFileResolver::new(sources))
608    }
609
610    /// Adds static binary files embedded in memory (e.g., images).
611    ///
612    /// # Example
613    ///
614    /// ```rust,no_run
615    /// # use typst_as_lib::TypstEngine;
616    /// static TEMPLATE: &str = r#"#image("logo.png")"#;
617    /// static LOGO: &[u8] = include_bytes!("../examples/templates/images/typst.png");
618    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
619    ///
620    /// let engine = TypstEngine::builder()
621    ///     .main_file(TEMPLATE)
622    ///     .fonts([FONT])
623    ///     .with_static_file_resolver([("logo.png", LOGO)])
624    ///     .build();
625    /// ```
626    ///
627    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
628    pub fn with_static_file_resolver<IB, F, B>(self, binaries: IB) -> Self
629    where
630        IB: IntoIterator<Item = (F, B)>,
631        F: IntoFileId,
632        B: IntoBytes,
633    {
634        self.add_file_resolver(StaticFileResolver::new(binaries))
635    }
636
637    /// Enables loading files from the file system.
638    ///
639    /// Files are resolved relative to `root`. Files outside of `root` cannot be accessed.
640    ///
641    /// # Example
642    ///
643    /// ```rust,no_run
644    /// # use typst_as_lib::TypstEngine;
645    /// static TEMPLATE: &str = r#"#include "header.typ""#;
646    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
647    ///
648    /// let engine = TypstEngine::builder()
649    ///     .main_file(TEMPLATE)
650    ///     .fonts([FONT])
651    ///     .with_file_system_resolver("./templates")
652    ///     .build();
653    /// ```
654    ///
655    /// See also: [resolve_packages.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_packages.rs)
656    pub fn with_file_system_resolver<P>(self, root: P) -> Self
657    where
658        P: Into<PathBuf>,
659    {
660        self.add_file_resolver(FileSystemResolver::new(root.into()).into_cached())
661    }
662
663    /// Sets the maximum age for comemo cache eviction after compilation.
664    ///
665    /// Default is `Some(0)`, which evicts after each compilation.
666    pub fn comemo_evict_max_age(&mut self, comemo_evict_max_age: Option<usize>) -> &mut Self {
667        self.comemo_evict_max_age = comemo_evict_max_age;
668        self
669    }
670
671    /// Enables downloading packages from the Typst package repository.
672    ///
673    /// Packages are cached on the file system for reuse.
674    ///
675    /// # Example
676    ///
677    /// ```rust,no_run
678    /// # use typst_as_lib::TypstEngine;
679    /// static TEMPLATE: &str = r#"#import "@preview/example:0.1.0": *"#;
680    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
681    ///
682    /// let engine = TypstEngine::builder()
683    ///     .main_file(TEMPLATE)
684    ///     .fonts([FONT])
685    ///     .with_package_file_resolver()
686    ///     .build();
687    /// ```
688    ///
689    /// See also: [resolve_packages.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_packages.rs)
690    #[cfg(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
691    pub fn with_package_file_resolver(self) -> Self {
692        use package_resolver::PackageResolver;
693        let file_resolver = PackageResolver::builder()
694            .with_file_system_cache()
695            .build()
696            .into_cached();
697        self.add_file_resolver(file_resolver)
698    }
699
700    /// Builds the [`TypstEngine`] with the configured options.
701    pub fn build(self) -> TypstEngine<T> {
702        let TypstTemplateEngineBuilder {
703            template,
704            inject_location,
705            file_resolvers,
706            comemo_evict_max_age,
707            fonts,
708            #[cfg(feature = "typst-kit-fonts")]
709            typst_kit_font_options,
710        } = self;
711
712        let mut book = FontBook::new();
713        if let Some(fonts) = &fonts {
714            for f in fonts {
715                book.push(f.info().clone());
716            }
717        }
718
719        #[allow(unused_mut)]
720        let mut fonts: Vec<_> = fonts.into_iter().flatten().map(FontEnum::Font).collect();
721
722        #[cfg(feature = "typst-kit-fonts")]
723        if let Some(typst_kit_font_options) = typst_kit_font_options {
724            let typst_kit_options::TypstKitFontOptions {
725                include_system_fonts,
726                include_dirs,
727                #[cfg(feature = "typst-kit-embed-fonts")]
728                include_embedded_fonts,
729            } = typst_kit_font_options;
730            let mut searcher = typst_kit::fonts::Fonts::searcher();
731            #[cfg(feature = "typst-kit-embed-fonts")]
732            searcher.include_embedded_fonts(include_embedded_fonts);
733            let typst_kit::fonts::Fonts {
734                book: typst_kit_book,
735                fonts: typst_kit_fonts,
736            } = searcher
737                .include_system_fonts(include_system_fonts)
738                .search_with(include_dirs);
739            let len = typst_kit_fonts.len();
740            let font_slots = typst_kit_fonts.into_iter().map(FontEnum::FontSlot);
741            if fonts.is_empty() {
742                book = typst_kit_book;
743                fonts = font_slots.collect();
744            } else {
745                for i in 0..len {
746                    let Some(info) = typst_kit_book.info(i) else {
747                        break;
748                    };
749                    book.push(info.clone());
750                }
751                fonts.extend(font_slots);
752            }
753        }
754
755        #[cfg(not(feature = "typst-html"))]
756        let library = typst::Library::builder().build();
757
758        #[cfg(feature = "typst-html")]
759        let library = typst::Library::builder()
760            .with_features([typst::Feature::Html].into_iter().collect())
761            .build();
762
763        TypstEngine {
764            template,
765            inject_location,
766            file_resolvers,
767            comemo_evict_max_age,
768            library: LazyHash::new(library),
769            book: LazyHash::new(book),
770            fonts,
771        }
772    }
773}
774
775/// The Typst world instance used for compilation.
776///
777/// Borrows its configuration from a [`TypstEngine`]. Constructed via
778/// [`TypstEngine::world_builder`] or [`TypstEngine::with_world`].
779pub struct TypstWorld<'a> {
780    library: Cow<'a, LazyHash<Library>>,
781    main_source_id: FileId,
782    now: DateTime<Utc>,
783    book: &'a LazyHash<FontBook>,
784    file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
785    fonts: &'a [FontEnum],
786}
787
788impl typst::World for TypstWorld<'_> {
789    fn library(&self) -> &LazyHash<Library> {
790        self.library.as_ref()
791    }
792
793    fn book(&self) -> &LazyHash<FontBook> {
794        self.book
795    }
796
797    fn main(&self) -> FileId {
798        self.main_source_id
799    }
800
801    fn source(&self, id: FileId) -> FileResult<Source> {
802        let Self { file_resolvers, .. } = *self;
803        let mut last_error = not_found(id);
804        for file_resolver in file_resolvers {
805            match file_resolver.resolve_source(id) {
806                Ok(source) => return Ok(source.into_owned()),
807                Err(error) => last_error = error,
808            }
809        }
810        Err(last_error)
811    }
812
813    fn file(&self, id: FileId) -> FileResult<Bytes> {
814        let Self { file_resolvers, .. } = *self;
815        let mut last_error = not_found(id);
816        for file_resolver in file_resolvers {
817            match file_resolver.resolve_binary(id) {
818                Ok(file) => return Ok(file.into_owned()),
819                Err(error) => last_error = error,
820            }
821        }
822        Err(last_error)
823    }
824
825    fn font(&self, id: usize) -> Option<Font> {
826        self.fonts[id].get()
827    }
828
829    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
830        let mut now = self.now;
831        if let Some(offset) = offset {
832            now += Duration::hours(offset);
833        }
834        let date = now.date_naive();
835        let year = date.year();
836        let month = (date.month0() + 1) as u8;
837        let day = (date.day0() + 1) as u8;
838        Datetime::from_ymd(year, month, day)
839    }
840}
841
842/// Builder for constructing a [`TypstWorld`] from a [`TypstEngine`].
843///
844/// Obtained via [`TypstEngine::world_builder`]. Call [`with_inputs`](Self::with_inputs)
845/// optionally, then [`build`](Self::build) to get the world.
846pub struct TypstWorldBuilder<'a, T> {
847    engine: &'a TypstEngine<T>,
848    main_source_id: FileId,
849    inputs: Option<Dict>,
850}
851
852impl<'a, T> TypstWorldBuilder<'a, T> {
853    fn new(engine: &'a TypstEngine<T>, main_source_id: FileId) -> Self {
854        Self {
855            engine,
856            main_source_id,
857            inputs: None,
858        }
859    }
860
861    /// Injects a `Dict` as `sys.inputs` into the compiled document.
862    pub fn with_inputs<D: Into<Dict>>(mut self, inputs: D) -> Self {
863        self.inputs = Some(inputs.into());
864        self
865    }
866
867    /// Builds the [`TypstWorld`]. Returns an error if input injection fails.
868    pub fn build(self) -> Result<TypstWorld<'a>, TypstAsLibError> {
869        let library = if let Some(inputs) = self.inputs {
870            Cow::Owned(self.engine.create_injected_library(inputs)?)
871        } else {
872            Cow::Borrowed(&self.engine.library)
873        };
874
875        Ok(TypstWorld {
876            main_source_id: self.main_source_id,
877            library,
878            now: Utc::now(),
879            file_resolvers: &self.engine.file_resolvers,
880            book: &self.engine.book,
881            fonts: &self.engine.fonts,
882        })
883    }
884}
885
886#[derive(Debug, Clone)]
887struct InjectLocation {
888    module_name: &'static str,
889    value_name: &'static str,
890}
891
892/// Errors that can occur when using typst-as-lib.
893#[derive(Debug, Clone, Error)]
894pub enum TypstAsLibError {
895    /// Errors from Typst source compilation.
896    #[error("Typst source error: {0:?}")]
897    TypstSource(EcoVec<SourceDiagnostic>),
898    /// Errors from file operations.
899    #[error("Typst file error: {0}")]
900    TypstFile(#[from] FileError),
901    /// The specified main source file was not found.
902    #[error("Source file does not exist in collection: {0:?}")]
903    MainSourceFileDoesNotExist(FileId),
904    /// Errors with additional hints from Typst.
905    #[error("Typst hinted String: {0:?}")]
906    HintedString(HintedString),
907    /// Other unspecified errors.
908    #[error("Unspecified: {0}!")]
909    Unspecified(ecow::EcoString),
910}
911
912impl From<HintedString> for TypstAsLibError {
913    fn from(value: HintedString) -> Self {
914        TypstAsLibError::HintedString(value)
915    }
916}
917
918impl From<ecow::EcoString> for TypstAsLibError {
919    fn from(value: ecow::EcoString) -> Self {
920        TypstAsLibError::Unspecified(value)
921    }
922}
923
924impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
925    fn from(value: EcoVec<SourceDiagnostic>) -> Self {
926        TypstAsLibError::TypstSource(value)
927    }
928}
929
930/// Wrapper for different font types.
931#[derive(Debug)]
932pub enum FontEnum {
933    /// A directly loaded font.
934    Font(Font),
935    /// A lazy font slot from typst-kit.
936    #[cfg(feature = "typst-kit-fonts")]
937    FontSlot(typst_kit::fonts::FontSlot),
938}
939
940impl FontEnum {
941    /// Retrieves the font, loading it if necessary.
942    pub fn get(&self) -> Option<Font> {
943        match self {
944            FontEnum::Font(font) => Some(font.clone()),
945            #[cfg(feature = "typst-kit-fonts")]
946            FontEnum::FontSlot(font_slot) => font_slot.get(),
947        }
948    }
949}