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::Output;
22use typst::foundations::{Bytes, Datetime, Dict, Module, Scope, Value};
23use typst::syntax::{FileId, Source};
24use typst::text::{Font, FontBook};
25use typst::utils::LazyHash;
26use typst::{Library, LibraryExt};
27use util::not_found;
28
29/// Caching wrapper for file resolvers.
30pub mod cached_file_resolver;
31/// Type conversion traits for Typst types.
32pub mod conversions;
33/// File resolution for Typst sources and binaries.
34pub mod file_resolver;
35pub(crate) mod util;
36
37#[cfg(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
38/// Package resolution and downloading from the Typst package repository.
39pub mod package_resolver;
40
41#[cfg(feature = "typst-kit-fonts")]
42/// Configuration options for `typst-kit` font searching.
43pub mod typst_kit_options;
44
45/// Main entry point for compiling Typst documents.
46///
47/// Use [`TypstEngine::builder()`] to construct an instance. You can optionally set a
48/// main file with [`main_file()`](TypstTemplateEngineBuilder::main_file), which allows
49/// compiling without specifying the file ID each time.
50///
51/// # Examples
52///
53/// With main file (compile without file ID):
54///
55/// ```rust,no_run
56/// # use typst_as_lib::TypstEngine;
57/// # use typst_layout::PagedDocument;
58/// static TEMPLATE: &str = "Hello World!";
59/// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
60///
61/// let engine = TypstEngine::builder()
62///     .main_file(TEMPLATE)
63///     .fonts([FONT])
64///     .build();
65///
66/// // Compile the main file directly
67/// let doc: PagedDocument = engine.compile().output.expect("Compilation failed");
68/// ```
69///
70/// Without main file (must provide file ID):
71///
72/// ```rust,no_run
73/// # use typst_as_lib::TypstEngine;
74/// # use typst_layout::PagedDocument;
75/// static TEMPLATE: &str = "Hello World!";
76/// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
77///
78/// let engine = TypstEngine::builder()
79///     .fonts([FONT])
80///     .with_static_source_file_resolver([("template.typ", TEMPLATE)])
81///     .build();
82///
83/// // Must specify file ID for each compile
84/// let doc: PagedDocument = engine.compile("template.typ").output.expect("Compilation failed");
85/// ```
86///
87/// See also: [Examples directory](https://github.com/Relacibo/typst-as-lib/tree/main/examples)
88pub struct TypstEngine<T = TypstTemplateCollection> {
89    template: T,
90    book: LazyHash<FontBook>,
91    inject_location: Option<InjectLocation>,
92    file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
93    library: LazyHash<Library>,
94    comemo_evict_max_age: Option<usize>,
95    fonts: Vec<FontEnum>,
96}
97
98/// Type state indicating no main file is set.
99#[derive(Debug, Clone, Copy)]
100pub struct TypstTemplateCollection;
101
102/// Type state indicating a main file has been set.
103#[derive(Debug, Clone, Copy)]
104pub struct TypstTemplateMainFile {
105    source_id: FileId,
106}
107
108impl<T> TypstEngine<T> {
109    fn do_compile<Doc>(
110        &self,
111        main_source_id: FileId,
112        inputs: Option<Dict>,
113    ) -> Warned<Result<Doc, TypstAsLibError>>
114    where
115        Doc: Output,
116    {
117        let mut builder = TypstWorldBuilder::new(self, main_source_id);
118        if let Some(inputs) = inputs {
119            builder = builder.with_inputs(inputs);
120        }
121        let world = match builder.build() {
122            Ok(world) => world,
123            Err(err) => {
124                return Warned {
125                    output: Err(err),
126                    warnings: Default::default(),
127                };
128            }
129        };
130        let Warned { output, warnings } = typst::compile(&world);
131        if let Some(max_age) = self.comemo_evict_max_age {
132            comemo::evict(max_age);
133        }
134        Warned {
135            output: output.map_err(Into::into),
136            warnings,
137        }
138    }
139
140    fn create_injected_library<D>(&self, input: D) -> Result<LazyHash<Library>, TypstAsLibError>
141    where
142        D: Into<Dict>,
143    {
144        let Self {
145            inject_location,
146            library,
147            ..
148        } = self;
149        let mut lib = library.deref().clone();
150        let (module_name, value_name) = if let Some(InjectLocation {
151            module_name,
152            value_name,
153        }) = inject_location
154        {
155            (*module_name, *value_name)
156        } else {
157            ("sys", "inputs")
158        };
159        {
160            let global = lib.global.scope_mut();
161            let input_dict: Dict = input.into();
162            if let Some(module_value) = global.get_mut(module_name) {
163                let module_value = module_value.write()?;
164                if let Value::Module(module) = module_value {
165                    let scope = module.scope_mut();
166                    if let Some(target) = scope.get_mut(value_name) {
167                        // Override existing field
168                        *target.write()? = Value::Dict(input_dict);
169                    } else {
170                        // Write new field into existing module scope
171                        scope.define(value_name, input_dict);
172                    }
173                } else {
174                    // Override existing non module value
175                    let mut scope = Scope::deduplicating();
176                    scope.define(value_name, input_dict);
177                    let module = Module::new(module_name, scope);
178                    *module_value = Value::Module(module);
179                }
180            } else {
181                // Create new module and field
182                let mut scope = Scope::deduplicating();
183                scope.define(value_name, input_dict);
184                let module = Module::new(module_name, scope);
185                global.define(module_name, module);
186            }
187        }
188        Ok(LazyHash::new(lib))
189    }
190}
191
192impl TypstEngine<TypstTemplateCollection> {
193    /// Creates a new builder for configuring a [`TypstEngine`].
194    ///
195    /// # Example
196    ///
197    /// ```rust,no_run
198    /// # use typst_as_lib::TypstEngine;
199    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
200    ///
201    /// let engine = TypstEngine::builder()
202    ///     .fonts([FONT])
203    ///     .build();
204    /// ```
205    pub fn builder() -> TypstTemplateEngineBuilder {
206        TypstTemplateEngineBuilder::default()
207    }
208}
209
210impl TypstEngine<TypstTemplateCollection> {
211    /// Compiles a Typst document with input data injected as `sys.inputs`.
212    ///
213    /// The input will be available in Typst scripts via `#import sys: inputs`.
214    ///
215    /// To change the injection location, use [`custom_inject_location()`](TypstTemplateEngineBuilder::custom_inject_location).
216    ///
217    /// # Example
218    ///
219    /// ```rust,no_run
220    /// # use typst_as_lib::TypstEngine;
221    /// # use typst::foundations::{Dict, IntoValue};
222    /// # use typst_layout::PagedDocument;
223    /// static TEMPLATE: &str = "#import sys: inputs\n#inputs.name";
224    /// static FONT: &[u8] = include_bytes!("../examples/fonts/texgyrecursor-regular.otf");
225    ///
226    /// let engine = TypstEngine::builder()
227    ///     .fonts([FONT])
228    ///     .with_static_source_file_resolver([("main.typ", TEMPLATE)])
229    ///     .build();
230    ///
231    /// let mut inputs = Dict::new();
232    /// inputs.insert("name".into(), "World".into_value());
233    ///
234    /// let doc: PagedDocument = engine.compile_with_input("main.typ", inputs)
235    ///     .output
236    ///     .expect("Compilation failed");
237    /// ```
238    ///
239    /// See also: [resolve_static.rs](https://github.com/Relacibo/typst-as-lib/blob/main/examples/resolve_static.rs)
240    pub fn compile_with_input<F, D, Doc>(
241        &self,
242        main_source_id: F,
243        inputs: D,
244    ) -> Warned<Result<Doc, TypstAsLibError>>
245    where
246        F: IntoFileId,
247        D: Into<Dict>,
248        Doc: Output,
249    {
250        self.do_compile(main_source_id.into_file_id(), Some(inputs.into()))
251    }
252
253    /// Compiles a Typst document without input data.
254    pub fn compile<F, Doc>(&self, main_source_id: F) -> Warned<Result<Doc, TypstAsLibError>>
255    where
256        F: IntoFileId,
257        Doc: Output,
258    {
259        self.do_compile(main_source_id.into_file_id(), None)
260    }
261
262    /// Returns a [`TypstWorldBuilder`] for constructing a [`TypstWorld`] bound to a specific file.
263    ///
264    /// This is an advanced low-level API. The caller is responsible for driving compilation
265    /// (e.g. via `typst::compile`) and for managing the `comemo` cache afterwards.
266    /// No cache eviction is performed automatically — use [`with_world`](Self::with_world)
267    /// if you want eviction handled for you.
268    ///
269    /// # Example
270    /// ```rust,ignore
271    /// # use typst_as_lib::TypstEngine;
272    /// let engine = TypstEngine::builder().build();
273    ///
274    /// let world = engine.world_builder("/main.typ")
275    ///     .with_inputs(my_inputs)
276    ///     .build()?;
277    /// let doc = typst::compile(&world).output.expect("Failed");
278    /// comemo::evict(30); // caller manages cache eviction
279    /// ```
280    pub fn world_builder<I>(
281        &self,
282        main_source_id: I,
283    ) -> TypstWorldBuilder<'_, TypstTemplateCollection>
284    where
285        I: IntoFileId,
286    {
287        TypstWorldBuilder::new(self, main_source_id.into_file_id())
288    }
289
290    /// Execute a closure with a [`TypstWorld`] for a specific file,
291    /// optionally injecting custom inputs.
292    ///
293    /// Runs the `comemo` cache eviction after the closure returns.
294    /// For full control use [`world_builder`](Self::world_builder) instead.
295    ///
296    /// # Example
297    /// ```rust,ignore
298    /// # use typst_as_lib::TypstEngine;
299    /// let engine = TypstEngine::builder().build();
300    ///
301    /// let pdf_bytes = engine.with_world("/main.typ", |world| {
302    ///     let doc = typst::compile(world).output.expect("Failed");
303    ///     typst_pdf::pdf(&doc, Default::default()).expect("Failed")
304    /// }).unwrap();
305    ///
306    /// // With inputs:
307    /// let pdf_bytes = engine.with_world("/main.typ", |world| { ... }).unwrap();
308    /// ```
309    pub fn with_world<F, I, R>(&self, main_source_id: I, f: F) -> 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: Output,
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: Output,
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 tk_book = typst::text::FontBook::new();
731            let mut tk_fonts: Vec<FontEnum> = Vec::new();
732
733            #[cfg(feature = "typst-kit-embed-fonts")]
734            if include_embedded_fonts {
735                for (font, info) in typst_kit::fonts::embedded() {
736                    tk_book.push(info);
737                    tk_fonts.push(FontEnum::Font(font));
738                }
739            }
740
741            if include_system_fonts {
742                for (font_path, info) in typst_kit::fonts::system() {
743                    tk_book.push(info);
744                    tk_fonts.push(FontEnum::FontPath(font_path));
745                }
746            }
747            for dir in &include_dirs {
748                for (font_path, info) in typst_kit::fonts::scan(dir) {
749                    tk_book.push(info);
750                    tk_fonts.push(FontEnum::FontPath(font_path));
751                }
752            }
753
754            let len = tk_fonts.len();
755            if fonts.is_empty() {
756                book = tk_book;
757                fonts = tk_fonts;
758            } else {
759                for i in 0..len {
760                    let Some(info) = tk_book.info(i) else {
761                        break;
762                    };
763                    book.push(info.clone());
764                }
765                fonts.extend(tk_fonts);
766            }
767        }
768
769        #[cfg(not(feature = "typst-html"))]
770        let library = typst::Library::builder().build();
771
772        #[cfg(feature = "typst-html")]
773        let library = typst::Library::builder()
774            .with_features([typst::Feature::Html].into_iter().collect())
775            .build();
776
777        TypstEngine {
778            template,
779            inject_location,
780            file_resolvers,
781            comemo_evict_max_age,
782            library: LazyHash::new(library),
783            book: LazyHash::new(book),
784            fonts,
785        }
786    }
787}
788
789/// The Typst world instance used for compilation.
790///
791/// Borrows its configuration from a [`TypstEngine`]. Constructed via
792/// [`TypstEngine::world_builder`] or [`TypstEngine::with_world`].
793pub struct TypstWorld<'a> {
794    library: Cow<'a, LazyHash<Library>>,
795    main_source_id: FileId,
796    now: DateTime<Utc>,
797    book: &'a LazyHash<FontBook>,
798    file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
799    fonts: &'a [FontEnum],
800}
801
802impl typst::World for TypstWorld<'_> {
803    fn library(&self) -> &LazyHash<Library> {
804        self.library.as_ref()
805    }
806
807    fn book(&self) -> &LazyHash<FontBook> {
808        self.book
809    }
810
811    fn main(&self) -> FileId {
812        self.main_source_id
813    }
814
815    fn source(&self, id: FileId) -> FileResult<Source> {
816        let Self { file_resolvers, .. } = *self;
817        let mut last_error = not_found(id);
818        for file_resolver in file_resolvers {
819            match file_resolver.resolve_source(id) {
820                Ok(source) => return Ok(source.into_owned()),
821                Err(error) => last_error = error,
822            }
823        }
824        Err(last_error)
825    }
826
827    fn file(&self, id: FileId) -> FileResult<Bytes> {
828        let Self { file_resolvers, .. } = *self;
829        let mut last_error = not_found(id);
830        for file_resolver in file_resolvers {
831            match file_resolver.resolve_binary(id) {
832                Ok(file) => return Ok(file.into_owned()),
833                Err(error) => last_error = error,
834            }
835        }
836        Err(last_error)
837    }
838
839    fn font(&self, id: usize) -> Option<Font> {
840        self.fonts[id].get()
841    }
842
843    fn today(&self, offset: Option<typst::foundations::Duration>) -> Option<Datetime> {
844        let mut now = self.now;
845        if let Some(offset) = offset {
846            now += Duration::hours(offset.hours() as i64);
847        }
848        let date = now.date_naive();
849        let year = date.year();
850        let month = (date.month0() + 1) as u8;
851        let day = (date.day0() + 1) as u8;
852        Datetime::from_ymd(year, month, day)
853    }
854}
855
856/// Builder for constructing a [`TypstWorld`] from a [`TypstEngine`].
857///
858/// Obtained via [`TypstEngine::world_builder`]. Call [`with_inputs`](Self::with_inputs)
859/// optionally, then [`build`](Self::build) to get the world.
860pub struct TypstWorldBuilder<'a, T> {
861    engine: &'a TypstEngine<T>,
862    main_source_id: FileId,
863    inputs: Option<Dict>,
864}
865
866impl<'a, T> TypstWorldBuilder<'a, T> {
867    fn new(engine: &'a TypstEngine<T>, main_source_id: FileId) -> Self {
868        Self {
869            engine,
870            main_source_id,
871            inputs: None,
872        }
873    }
874
875    /// Injects a `Dict` as `sys.inputs` into the compiled document.
876    pub fn with_inputs<D: Into<Dict>>(mut self, inputs: D) -> Self {
877        self.inputs = Some(inputs.into());
878        self
879    }
880
881    /// Builds the [`TypstWorld`]. Returns an error if input injection fails.
882    pub fn build(self) -> Result<TypstWorld<'a>, TypstAsLibError> {
883        let library = if let Some(inputs) = self.inputs {
884            Cow::Owned(self.engine.create_injected_library(inputs)?)
885        } else {
886            Cow::Borrowed(&self.engine.library)
887        };
888
889        Ok(TypstWorld {
890            main_source_id: self.main_source_id,
891            library,
892            now: Utc::now(),
893            file_resolvers: &self.engine.file_resolvers,
894            book: &self.engine.book,
895            fonts: &self.engine.fonts,
896        })
897    }
898}
899
900#[derive(Debug, Clone)]
901struct InjectLocation {
902    module_name: &'static str,
903    value_name: &'static str,
904}
905
906/// Errors that can occur when using typst-as-lib.
907#[derive(Debug, Clone, Error)]
908pub enum TypstAsLibError {
909    /// Errors from Typst source compilation.
910    #[error("Typst source error: {0:?}")]
911    TypstSource(EcoVec<SourceDiagnostic>),
912    /// Errors from file operations.
913    #[error("Typst file error: {0}")]
914    TypstFile(#[from] FileError),
915    /// The specified main source file was not found.
916    #[error("Source file does not exist in collection: {0:?}")]
917    MainSourceFileDoesNotExist(FileId),
918    /// Errors with additional hints from Typst.
919    #[error("Typst hinted String: {0:?}")]
920    HintedString(HintedString),
921    /// Other unspecified errors.
922    #[error("Unspecified: {0}!")]
923    Unspecified(ecow::EcoString),
924}
925
926impl From<HintedString> for TypstAsLibError {
927    fn from(value: HintedString) -> Self {
928        TypstAsLibError::HintedString(value)
929    }
930}
931
932impl From<ecow::EcoString> for TypstAsLibError {
933    fn from(value: ecow::EcoString) -> Self {
934        TypstAsLibError::Unspecified(value)
935    }
936}
937
938impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
939    fn from(value: EcoVec<SourceDiagnostic>) -> Self {
940        TypstAsLibError::TypstSource(value)
941    }
942}
943
944/// Wrapper for different font types.
945#[derive(Debug)]
946pub enum FontEnum {
947    /// A directly loaded font.
948    Font(Font),
949    /// A lazy font slot from typst-kit.
950    #[cfg(feature = "typst-kit-fonts")]
951    FontPath(typst_kit::fonts::FontPath),
952}
953
954impl FontEnum {
955    /// Retrieves the font, loading it if necessary.
956    pub fn get(&self) -> Option<Font> {
957        match self {
958            FontEnum::Font(font) => Some(font.clone()),
959            #[cfg(feature = "typst-kit-fonts")]
960            FontEnum::FontPath(font_path) => {
961                use typst_kit::fonts::FontSource;
962                font_path.load()
963            }
964        }
965    }
966}