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(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
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(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
380 pub fn with_package_file_resolver(self) -> Self {
389 use package_resolver::PackageResolver;
390 let file_resolver = PackageResolver::builder()
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
415 #[allow(unused_mut)]
416 let mut fonts: Vec<_> = fonts.into_iter().flatten().map(FontEnum::Font).collect();
417
418 #[cfg(feature = "typst-kit-fonts")]
419 if let Some(typst_kit_font_options) = typst_kit_font_options {
420 let typst_kit_options::TypstKitFontOptions {
421 include_system_fonts,
422 include_dirs,
423 #[cfg(feature = "typst-kit-embed-fonts")]
424 include_embedded_fonts,
425 } = typst_kit_font_options;
426 let mut searcher = typst_kit::fonts::Fonts::searcher();
427 #[cfg(feature = "typst-kit-embed-fonts")]
428 searcher.include_embedded_fonts(include_embedded_fonts);
429 let typst_kit::fonts::Fonts {
430 book: typst_kit_book,
431 fonts: typst_kit_fonts,
432 } = searcher
433 .include_system_fonts(include_system_fonts)
434 .search_with(include_dirs);
435 let len = typst_kit_fonts.len();
436 let font_slots = typst_kit_fonts.into_iter().map(FontEnum::FontSlot);
437 if fonts.is_empty() {
438 book = typst_kit_book;
439 fonts = font_slots.collect();
440 } else {
441 for i in 0..len {
442 let Some(info) = typst_kit_book.info(i) else {
443 break;
444 };
445 book.push(info.clone());
446 }
447 fonts.extend(font_slots);
448 }
449 }
450
451 #[cfg(not(feature = "typst-html"))]
452 let library = Default::default();
453
454 #[cfg(feature = "typst-html")]
455 let library = typst::Library::builder()
456 .with_features([typst::Feature::Html].into_iter().collect())
457 .build();
458
459 TypstEngine {
460 template,
461 inject_location,
462 file_resolvers,
463 comemo_evict_max_age,
464 library: LazyHash::new(library),
465 book: LazyHash::new(book),
466 fonts,
467 }
468 }
469}
470
471struct TypstWorld<'a> {
472 library: Cow<'a, LazyHash<Library>>,
473 main_source_id: FileId,
474 now: DateTime<Utc>,
475 book: &'a LazyHash<FontBook>,
476 file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
477 fonts: &'a [FontEnum],
478}
479
480impl typst::World for TypstWorld<'_> {
481 fn library(&self) -> &LazyHash<Library> {
482 self.library.as_ref()
483 }
484
485 fn book(&self) -> &LazyHash<FontBook> {
486 self.book
487 }
488
489 fn main(&self) -> FileId {
490 self.main_source_id
491 }
492
493 fn source(&self, id: FileId) -> FileResult<Source> {
494 let Self { file_resolvers, .. } = *self;
495 let mut last_error = not_found(id);
496 for file_resolver in file_resolvers {
497 match file_resolver.resolve_source(id) {
498 Ok(source) => return Ok(source.into_owned()),
499 Err(error) => last_error = error,
500 }
501 }
502 Err(last_error)
503 }
504
505 fn file(&self, id: FileId) -> FileResult<Bytes> {
506 let Self { file_resolvers, .. } = *self;
507 let mut last_error = not_found(id);
508 for file_resolver in file_resolvers {
509 match file_resolver.resolve_binary(id) {
510 Ok(file) => return Ok(file.into_owned()),
511 Err(error) => last_error = error,
512 }
513 }
514 Err(last_error)
515 }
516
517 fn font(&self, id: usize) -> Option<Font> {
518 self.fonts[id].get()
519 }
520
521 fn today(&self, offset: Option<i64>) -> Option<Datetime> {
522 let mut now = self.now;
523 if let Some(offset) = offset {
524 now += Duration::hours(offset);
525 }
526 let date = now.date_naive();
527 let year = date.year();
528 let month = (date.month0() + 1) as u8;
529 let day = (date.day0() + 1) as u8;
530 Datetime::from_ymd(year, month, day)
531 }
532}
533
534#[derive(Debug, Clone)]
535struct InjectLocation {
536 module_name: &'static str,
537 value_name: &'static str,
538}
539
540#[derive(Debug, Clone, Error)]
541pub enum TypstAsLibError {
542 #[error("Typst source error: {0:?}")]
543 TypstSource(EcoVec<SourceDiagnostic>),
544 #[error("Typst file error: {0}")]
545 TypstFile(#[from] FileError),
546 #[error("Source file does not exist in collection: {0:?}")]
547 MainSourceFileDoesNotExist(FileId),
548 #[error("Typst hinted String: {0:?}")]
549 HintedString(HintedString),
550 #[error("Unspecified: {0}!")]
551 Unspecified(ecow::EcoString),
552}
553
554impl From<HintedString> for TypstAsLibError {
555 fn from(value: HintedString) -> Self {
556 TypstAsLibError::HintedString(value)
557 }
558}
559
560impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
561 fn from(value: EcoVec<SourceDiagnostic>) -> Self {
562 TypstAsLibError::TypstSource(value)
563 }
564}
565
566#[derive(Debug)]
567pub enum FontEnum {
568 Font(Font),
569 #[cfg(feature = "typst-kit-fonts")]
570 FontSlot(typst_kit::fonts::FontSlot),
571}
572
573impl FontEnum {
574 pub fn get(&self) -> Option<Font> {
575 match self {
576 FontEnum::Font(font) => Some(font.clone()),
577 #[cfg(feature = "typst-kit-fonts")]
578 FontEnum::FontSlot(font_slot) => font_slot.get(),
579 }
580 }
581}