1use std::borrow::Cow;
3use std::ops::Deref;
4use std::path::PathBuf;
5
6use cached_file_resolver::IntoCachedFileResolver;
7use chrono::{DateTime, Datelike, Duration, Utc};
8use conversions::{IntoBytes, IntoFileId, IntoFonts, IntoSource};
9use ecow::EcoVec;
10use file_resolver::{
11 FileResolver, FileSystemResolver, MainSourceFileResolver, StaticFileResolver,
12 StaticSourceFileResolver,
13};
14use thiserror::Error;
15use typst::diag::{FileError, FileResult, HintedString, SourceDiagnostic, Warned};
16use typst::foundations::{Bytes, Datetime, Dict, Module, Scope, Value};
17use typst::syntax::{FileId, Source};
18use typst::text::{Font, FontBook};
19use typst::utils::LazyHash;
20use typst::{Document, Library};
21use util::not_found;
22
23pub mod cached_file_resolver;
24pub mod conversions;
25pub mod file_resolver;
26pub(crate) mod util;
27
28#[cfg(feature = "packages")]
29pub mod package_resolver;
30
31#[cfg(feature = "typst-kit-fonts")]
32pub mod typst_kit_options;
33
34pub struct TypstEngine<T = TypstTemplateCollection> {
35 template: T,
36 book: LazyHash<FontBook>,
37 inject_location: Option<InjectLocation>,
38 file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
39 library: LazyHash<Library>,
40 comemo_evict_max_age: Option<usize>,
41 fonts: Vec<FontEnum>,
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct TypstTemplateCollection;
46
47#[derive(Debug, Clone, Copy)]
48pub struct TypstTemplateMainFile {
49 source_id: FileId,
50}
51
52impl<T> TypstEngine<T> {
53 fn do_compile<Doc>(
54 &self,
55 main_source_id: FileId,
56 inputs: Option<Dict>,
57 ) -> Warned<Result<Doc, TypstAsLibError>>
58 where
59 Doc: Document,
60 {
61 let library = if let Some(inputs) = inputs {
62 let lib = self.create_injected_library(inputs);
63 match lib {
64 Ok(lib) => Cow::Owned(lib),
65 Err(err) => {
66 return Warned {
67 output: Err(err),
68 warnings: Default::default(),
69 };
70 }
71 }
72 } else {
73 Cow::Borrowed(&self.library)
74 };
75 let world = TypstWorld {
76 main_source_id,
77 library,
78 now: Utc::now(),
79 file_resolvers: &self.file_resolvers,
80 book: &self.book,
81 fonts: &self.fonts,
82 };
83 let Warned { output, warnings } = typst::compile(&world);
84
85 if let Some(comemo_evict_max_age) = self.comemo_evict_max_age {
86 comemo::evict(comemo_evict_max_age);
87 }
88
89 Warned {
90 output: output.map_err(Into::into),
91 warnings,
92 }
93 }
94
95 fn create_injected_library<D>(&self, input: D) -> Result<LazyHash<Library>, TypstAsLibError>
96 where
97 D: Into<Dict>,
98 {
99 let Self {
100 inject_location,
101 library,
102 ..
103 } = self;
104 let mut lib = library.deref().clone();
105 let (module_name, value_name) = if let Some(InjectLocation {
106 module_name,
107 value_name,
108 }) = inject_location
109 {
110 (*module_name, *value_name)
111 } else {
112 ("sys", "inputs")
113 };
114 {
115 let global = lib.global.scope_mut();
116 let mut scope = Scope::new();
117 scope.define(value_name, input.into());
118 if let Some(value) = global.get_mut(module_name) {
119 let value = value.write().map_err(TypstAsLibError::Unspecified)?;
120 if let Value::Module(module) = value {
121 *module.scope_mut() = scope;
122 } else {
123 let module = Module::new(module_name, scope);
124 *value = Value::Module(module);
125 }
126 } else {
127 let module = Module::new(module_name, scope);
128 global.define(module_name, module);
129 }
130 }
131 Ok(LazyHash::new(lib))
132 }
133}
134
135impl TypstEngine<TypstTemplateCollection> {
136 pub fn builder() -> TypstTemplateEngineBuilder {
137 TypstTemplateEngineBuilder::default()
138 }
139}
140
141impl TypstEngine<TypstTemplateCollection> {
142 pub fn compile_with_input<F, D, Doc>(
161 &self,
162 main_source_id: F,
163 inputs: D,
164 ) -> Warned<Result<Doc, TypstAsLibError>>
165 where
166 F: IntoFileId,
167 D: Into<Dict>,
168 Doc: Document,
169 {
170 self.do_compile(main_source_id.into_file_id(), Some(inputs.into()))
171 }
172
173 pub fn compile<F, Doc>(&self, main_source_id: F) -> Warned<Result<Doc, TypstAsLibError>>
175 where
176 F: IntoFileId,
177 Doc: Document,
178 {
179 self.do_compile(main_source_id.into_file_id(), None)
180 }
181}
182
183impl TypstEngine<TypstTemplateMainFile> {
184 pub fn compile_with_input<D, Doc>(&self, inputs: D) -> Warned<Result<Doc, TypstAsLibError>>
203 where
204 D: Into<Dict>,
205 Doc: Document,
206 {
207 let TypstTemplateMainFile { source_id } = self.template;
208 self.do_compile(source_id, Some(inputs.into()))
209 }
210
211 pub fn compile<Doc>(&self) -> Warned<Result<Doc, TypstAsLibError>>
213 where
214 Doc: Document,
215 {
216 let TypstTemplateMainFile { source_id } = self.template;
217 self.do_compile(source_id, None)
218 }
219}
220
221pub struct TypstTemplateEngineBuilder<T = TypstTemplateCollection> {
222 template: T,
223 inject_location: Option<InjectLocation>,
224 file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
225 comemo_evict_max_age: Option<usize>,
226 fonts: Option<Vec<Font>>,
227 #[cfg(feature = "typst-kit-fonts")]
228 typst_kit_font_options: Option<typst_kit_options::TypstKitFontOptions>,
229}
230
231impl Default for TypstTemplateEngineBuilder {
232 fn default() -> Self {
233 Self {
234 template: TypstTemplateCollection,
235 inject_location: Default::default(),
236 file_resolvers: Default::default(),
237 comemo_evict_max_age: Some(0),
238 fonts: Default::default(),
239 #[cfg(feature = "typst-kit-fonts")]
240 typst_kit_font_options: None,
241 }
242 }
243}
244
245impl TypstTemplateEngineBuilder<TypstTemplateCollection> {
246 pub fn main_file<S: IntoSource>(
248 self,
249 source: S,
250 ) -> TypstTemplateEngineBuilder<TypstTemplateMainFile> {
251 let source = source.into_source();
252 let source_id = source.id();
253 let template = TypstTemplateMainFile { source_id };
254 let TypstTemplateEngineBuilder {
255 inject_location,
256 mut file_resolvers,
257 comemo_evict_max_age,
258 fonts,
259 #[cfg(feature = "typst-kit-fonts")]
260 typst_kit_font_options,
261 ..
262 } = self;
263 file_resolvers.push(Box::new(MainSourceFileResolver::new(source)));
264 TypstTemplateEngineBuilder {
265 template,
266 inject_location,
267 file_resolvers,
268 comemo_evict_max_age,
269 fonts,
270 #[cfg(feature = "typst-kit-fonts")]
271 typst_kit_font_options,
272 }
273 }
274}
275
276impl<T> TypstTemplateEngineBuilder<T> {
277 pub fn custom_inject_location(
281 mut self,
282 module_name: &'static str,
283 value_name: &'static str,
284 ) -> Self {
285 self.inject_location = Some(InjectLocation {
286 module_name,
287 value_name,
288 });
289 self
290 }
291
292 pub fn fonts<I, F>(mut self, fonts: I) -> Self
299 where
300 I: IntoIterator<Item = F>,
301 F: IntoFonts,
302 {
303 let fonts = fonts
304 .into_iter()
305 .flat_map(IntoFonts::into_fonts)
306 .collect::<Vec<_>>();
307 self.fonts = Some(fonts);
308 self
309 }
310
311 #[cfg(feature = "typst-kit-fonts")]
321 pub fn search_fonts_with(mut self, options: typst_kit_options::TypstKitFontOptions) -> Self {
322 self.typst_kit_font_options = Some(options);
323 self
324 }
325
326 pub fn add_file_resolver<F>(mut self, file_resolver: F) -> Self
330 where
331 F: FileResolver + Send + Sync + 'static,
332 {
333 self.file_resolvers.push(Box::new(file_resolver));
334 self
335 }
336
337 pub fn with_static_source_file_resolver<IS, S>(self, sources: IS) -> Self
348 where
349 IS: IntoIterator<Item = S>,
350 S: IntoSource,
351 {
352 self.add_file_resolver(StaticSourceFileResolver::new(sources))
353 }
354
355 pub fn with_static_file_resolver<IB, F, B>(self, binaries: IB) -> Self
357 where
358 IB: IntoIterator<Item = (F, B)>,
359 F: IntoFileId,
360 B: IntoBytes,
361 {
362 self.add_file_resolver(StaticFileResolver::new(binaries))
363 }
364
365 pub fn with_file_system_resolver<P>(self, root: P) -> Self
368 where
369 P: Into<PathBuf>,
370 {
371 self.add_file_resolver(FileSystemResolver::new(root.into()).into_cached())
372 }
373
374 pub fn comemo_evict_max_age(&mut self, comemo_evict_max_age: Option<usize>) -> &mut Self {
375 self.comemo_evict_max_age = comemo_evict_max_age;
376 self
377 }
378
379 #[cfg(feature = "packages")]
380 pub fn with_package_file_resolver(self) -> Self {
389 use package_resolver::PackageResolverBuilder;
390 let file_resolver = PackageResolverBuilder::new()
391 .with_file_system_cache()
392 .build()
393 .into_cached();
394 self.add_file_resolver(file_resolver)
395 }
396
397 pub fn build(self) -> TypstEngine<T> {
398 let TypstTemplateEngineBuilder {
399 template,
400 inject_location,
401 file_resolvers,
402 comemo_evict_max_age,
403 fonts,
404 #[cfg(feature = "typst-kit-fonts")]
405 typst_kit_font_options,
406 } = self;
407
408 let mut book = FontBook::new();
409 if let Some(fonts) = &fonts {
410 for f in fonts {
411 book.push(f.info().clone());
412 }
413 }
414 let mut fonts: Vec<_> = fonts.into_iter().flatten().map(FontEnum::Font).collect();
415
416 #[cfg(feature = "typst-kit-fonts")]
417 if let Some(typst_kit_font_options) = typst_kit_font_options {
418 let typst_kit_options::TypstKitFontOptions {
419 include_system_fonts,
420 include_dirs,
421 #[cfg(feature = "typst-kit-embed-fonts")]
422 include_embedded_fonts,
423 } = typst_kit_font_options;
424 let mut searcher = typst_kit::fonts::Fonts::searcher();
425 #[cfg(feature = "typst-kit-embed-fonts")]
426 searcher.include_embedded_fonts(include_embedded_fonts);
427 let typst_kit::fonts::Fonts {
428 book: typst_kit_book,
429 fonts: typst_kit_fonts,
430 } = searcher
431 .include_system_fonts(include_system_fonts)
432 .search_with(include_dirs);
433 let len = typst_kit_fonts.len();
434 let font_slots = typst_kit_fonts.into_iter().map(FontEnum::FontSlot);
435 if fonts.is_empty() {
436 book = typst_kit_book;
437 fonts = font_slots.collect();
438 } else {
439 for i in 0..len {
440 let Some(info) = typst_kit_book.info(i) else {
441 break;
442 };
443 book.push(info.clone());
444 }
445 fonts.extend(font_slots);
446 }
447 }
448
449 TypstEngine {
450 template,
451 inject_location,
452 file_resolvers,
453 comemo_evict_max_age,
454 library: Default::default(),
455 book: LazyHash::new(book),
456 fonts,
457 }
458 }
459}
460
461struct TypstWorld<'a> {
462 library: Cow<'a, LazyHash<Library>>,
463 main_source_id: FileId,
464 now: DateTime<Utc>,
465 book: &'a LazyHash<FontBook>,
466 file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
467 fonts: &'a [FontEnum],
468}
469
470impl typst::World for TypstWorld<'_> {
471 fn library(&self) -> &LazyHash<Library> {
472 self.library.as_ref()
473 }
474
475 fn book(&self) -> &LazyHash<FontBook> {
476 self.book
477 }
478
479 fn main(&self) -> FileId {
480 self.main_source_id
481 }
482
483 fn source(&self, id: FileId) -> FileResult<Source> {
484 let Self { file_resolvers, .. } = *self;
485 let mut last_error = not_found(id);
486 for file_resolver in file_resolvers {
487 match file_resolver.resolve_source(id) {
488 Ok(source) => return Ok(source.into_owned()),
489 Err(error) => last_error = error,
490 }
491 }
492 Err(last_error)
493 }
494
495 fn file(&self, id: FileId) -> FileResult<Bytes> {
496 let Self { file_resolvers, .. } = *self;
497 let mut last_error = not_found(id);
498 for file_resolver in file_resolvers {
499 match file_resolver.resolve_binary(id) {
500 Ok(file) => return Ok(file.into_owned()),
501 Err(error) => last_error = error,
502 }
503 }
504 Err(last_error)
505 }
506
507 fn font(&self, id: usize) -> Option<Font> {
508 self.fonts[id].get()
509 }
510
511 fn today(&self, offset: Option<i64>) -> Option<Datetime> {
512 let mut now = self.now;
513 if let Some(offset) = offset {
514 now += Duration::hours(offset);
515 }
516 let date = now.date_naive();
517 let year = date.year();
518 let month = (date.month0() + 1) as u8;
519 let day = (date.day0() + 1) as u8;
520 Datetime::from_ymd(year, month, day)
521 }
522}
523
524#[derive(Debug, Clone)]
525struct InjectLocation {
526 module_name: &'static str,
527 value_name: &'static str,
528}
529
530#[derive(Debug, Clone, Error)]
531pub enum TypstAsLibError {
532 #[error("Typst source error: {}", 0.to_string())]
533 TypstSource(EcoVec<SourceDiagnostic>),
534 #[error("Typst file error: {}", 0.to_string())]
535 TypstFile(#[from] FileError),
536 #[error("Source file does not exist in collection: {0:?}")]
537 MainSourceFileDoesNotExist(FileId),
538 #[error("Typst hinted String: {}", 0.to_string())]
539 HintedString(HintedString),
540 #[error("Unspecified: {0}!")]
541 Unspecified(ecow::EcoString),
542}
543
544impl From<HintedString> for TypstAsLibError {
545 fn from(value: HintedString) -> Self {
546 TypstAsLibError::HintedString(value)
547 }
548}
549
550impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
551 fn from(value: EcoVec<SourceDiagnostic>) -> Self {
552 TypstAsLibError::TypstSource(value)
553 }
554}
555
556#[derive(Debug)]
557pub enum FontEnum {
558 Font(Font),
559 #[cfg(feature = "typst-kit-fonts")]
560 FontSlot(typst_kit::fonts::FontSlot),
561}
562
563impl FontEnum {
564 pub fn get(&self) -> Option<Font> {
565 match self {
566 FontEnum::Font(font) => Some(font.clone()),
567 #[cfg(feature = "typst-kit-fonts")]
568 FontEnum::FontSlot(font_slot) => font_slot.get(),
569 }
570 }
571}