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}