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}