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, LibraryExt};
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>(
162 &self,
163 main_source_id: F,
164 inputs: D,
165 ) -> Warned<Result<Doc, TypstAsLibError>>
166 where
167 F: IntoFileId,
168 D: Into<Dict>,
169 Doc: Document,
170 {
171 self.do_compile(main_source_id.into_file_id(), Some(inputs.into()))
172 }
173
174 pub fn compile<F, Doc>(&self, main_source_id: F) -> Warned<Result<Doc, TypstAsLibError>>
176 where
177 F: IntoFileId,
178 Doc: Document,
179 {
180 self.do_compile(main_source_id.into_file_id(), None)
181 }
182}
183
184impl TypstEngine<TypstTemplateMainFile> {
185 pub fn compile_with_input<D, Doc>(&self, inputs: D) -> Warned<Result<Doc, TypstAsLibError>>
205 where
206 D: Into<Dict>,
207 Doc: Document,
208 {
209 let TypstTemplateMainFile { source_id } = self.template;
210 self.do_compile(source_id, Some(inputs.into()))
211 }
212
213 pub fn compile<Doc>(&self) -> Warned<Result<Doc, TypstAsLibError>>
215 where
216 Doc: Document,
217 {
218 let TypstTemplateMainFile { source_id } = self.template;
219 self.do_compile(source_id, None)
220 }
221}
222
223pub struct TypstTemplateEngineBuilder<T = TypstTemplateCollection> {
224 template: T,
225 inject_location: Option<InjectLocation>,
226 file_resolvers: Vec<Box<dyn FileResolver + Send + Sync + 'static>>,
227 comemo_evict_max_age: Option<usize>,
228 fonts: Option<Vec<Font>>,
229 #[cfg(feature = "typst-kit-fonts")]
230 typst_kit_font_options: Option<typst_kit_options::TypstKitFontOptions>,
231}
232
233impl Default for TypstTemplateEngineBuilder {
234 fn default() -> Self {
235 Self {
236 template: TypstTemplateCollection,
237 inject_location: Default::default(),
238 file_resolvers: Default::default(),
239 comemo_evict_max_age: Some(0),
240 fonts: Default::default(),
241 #[cfg(feature = "typst-kit-fonts")]
242 typst_kit_font_options: None,
243 }
244 }
245}
246
247impl TypstTemplateEngineBuilder<TypstTemplateCollection> {
248 pub fn main_file<S: IntoSource>(
250 self,
251 source: S,
252 ) -> TypstTemplateEngineBuilder<TypstTemplateMainFile> {
253 let source = source.into_source();
254 let source_id = source.id();
255 let template = TypstTemplateMainFile { source_id };
256 let TypstTemplateEngineBuilder {
257 inject_location,
258 mut file_resolvers,
259 comemo_evict_max_age,
260 fonts,
261 #[cfg(feature = "typst-kit-fonts")]
262 typst_kit_font_options,
263 ..
264 } = self;
265 file_resolvers.push(Box::new(MainSourceFileResolver::new(source)));
266 TypstTemplateEngineBuilder {
267 template,
268 inject_location,
269 file_resolvers,
270 comemo_evict_max_age,
271 fonts,
272 #[cfg(feature = "typst-kit-fonts")]
273 typst_kit_font_options,
274 }
275 }
276}
277
278impl<T> TypstTemplateEngineBuilder<T> {
279 pub fn custom_inject_location(
283 mut self,
284 module_name: &'static str,
285 value_name: &'static str,
286 ) -> Self {
287 self.inject_location = Some(InjectLocation {
288 module_name,
289 value_name,
290 });
291 self
292 }
293
294 pub fn fonts<I, F>(mut self, fonts: I) -> Self
301 where
302 I: IntoIterator<Item = F>,
303 F: IntoFonts,
304 {
305 let fonts = fonts
306 .into_iter()
307 .flat_map(IntoFonts::into_fonts)
308 .collect::<Vec<_>>();
309 self.fonts = Some(fonts);
310 self
311 }
312
313 #[cfg(feature = "typst-kit-fonts")]
322 pub fn search_fonts_with(mut self, options: typst_kit_options::TypstKitFontOptions) -> Self {
323 self.typst_kit_font_options = Some(options);
324 self
325 }
326
327 pub fn add_file_resolver<F>(mut self, file_resolver: F) -> Self
331 where
332 F: FileResolver + Send + Sync + 'static,
333 {
334 self.file_resolvers.push(Box::new(file_resolver));
335 self
336 }
337
338 pub fn with_static_source_file_resolver<IS, S>(self, sources: IS) -> Self
349 where
350 IS: IntoIterator<Item = S>,
351 S: IntoSource,
352 {
353 self.add_file_resolver(StaticSourceFileResolver::new(sources))
354 }
355
356 pub fn with_static_file_resolver<IB, F, B>(self, binaries: IB) -> Self
358 where
359 IB: IntoIterator<Item = (F, B)>,
360 F: IntoFileId,
361 B: IntoBytes,
362 {
363 self.add_file_resolver(StaticFileResolver::new(binaries))
364 }
365
366 pub fn with_file_system_resolver<P>(self, root: P) -> Self
369 where
370 P: Into<PathBuf>,
371 {
372 self.add_file_resolver(FileSystemResolver::new(root.into()).into_cached())
373 }
374
375 pub fn comemo_evict_max_age(&mut self, comemo_evict_max_age: Option<usize>) -> &mut Self {
376 self.comemo_evict_max_age = comemo_evict_max_age;
377 self
378 }
379
380 #[cfg(all(feature = "packages", any(feature = "ureq", feature = "reqwest")))]
381 pub fn with_package_file_resolver(self) -> Self {
392 use package_resolver::PackageResolver;
393 let file_resolver = PackageResolver::builder()
394 .with_file_system_cache()
395 .build()
396 .into_cached();
397 self.add_file_resolver(file_resolver)
398 }
399
400 pub fn build(self) -> TypstEngine<T> {
401 let TypstTemplateEngineBuilder {
402 template,
403 inject_location,
404 file_resolvers,
405 comemo_evict_max_age,
406 fonts,
407 #[cfg(feature = "typst-kit-fonts")]
408 typst_kit_font_options,
409 } = self;
410
411 let mut book = FontBook::new();
412 if let Some(fonts) = &fonts {
413 for f in fonts {
414 book.push(f.info().clone());
415 }
416 }
417
418 #[allow(unused_mut)]
419 let mut fonts: Vec<_> = fonts.into_iter().flatten().map(FontEnum::Font).collect();
420
421 #[cfg(feature = "typst-kit-fonts")]
422 if let Some(typst_kit_font_options) = typst_kit_font_options {
423 let typst_kit_options::TypstKitFontOptions {
424 include_system_fonts,
425 include_dirs,
426 #[cfg(feature = "typst-kit-embed-fonts")]
427 include_embedded_fonts,
428 } = typst_kit_font_options;
429 let mut searcher = typst_kit::fonts::Fonts::searcher();
430 #[cfg(feature = "typst-kit-embed-fonts")]
431 searcher.include_embedded_fonts(include_embedded_fonts);
432 let typst_kit::fonts::Fonts {
433 book: typst_kit_book,
434 fonts: typst_kit_fonts,
435 } = searcher
436 .include_system_fonts(include_system_fonts)
437 .search_with(include_dirs);
438 let len = typst_kit_fonts.len();
439 let font_slots = typst_kit_fonts.into_iter().map(FontEnum::FontSlot);
440 if fonts.is_empty() {
441 book = typst_kit_book;
442 fonts = font_slots.collect();
443 } else {
444 for i in 0..len {
445 let Some(info) = typst_kit_book.info(i) else {
446 break;
447 };
448 book.push(info.clone());
449 }
450 fonts.extend(font_slots);
451 }
452 }
453
454 #[cfg(not(feature = "typst-html"))]
455 let library = typst::Library::builder().build();
456
457 #[cfg(feature = "typst-html")]
458 let library = typst::Library::builder()
459 .with_features([typst::Feature::Html].into_iter().collect())
460 .build();
461
462 TypstEngine {
463 template,
464 inject_location,
465 file_resolvers,
466 comemo_evict_max_age,
467 library: LazyHash::new(library),
468 book: LazyHash::new(book),
469 fonts,
470 }
471 }
472}
473
474struct TypstWorld<'a> {
475 library: Cow<'a, LazyHash<Library>>,
476 main_source_id: FileId,
477 now: DateTime<Utc>,
478 book: &'a LazyHash<FontBook>,
479 file_resolvers: &'a [Box<dyn FileResolver + Send + Sync + 'static>],
480 fonts: &'a [FontEnum],
481}
482
483impl typst::World for TypstWorld<'_> {
484 fn library(&self) -> &LazyHash<Library> {
485 self.library.as_ref()
486 }
487
488 fn book(&self) -> &LazyHash<FontBook> {
489 self.book
490 }
491
492 fn main(&self) -> FileId {
493 self.main_source_id
494 }
495
496 fn source(&self, id: FileId) -> FileResult<Source> {
497 let Self { file_resolvers, .. } = *self;
498 let mut last_error = not_found(id);
499 for file_resolver in file_resolvers {
500 match file_resolver.resolve_source(id) {
501 Ok(source) => return Ok(source.into_owned()),
502 Err(error) => last_error = error,
503 }
504 }
505 Err(last_error)
506 }
507
508 fn file(&self, id: FileId) -> FileResult<Bytes> {
509 let Self { file_resolvers, .. } = *self;
510 let mut last_error = not_found(id);
511 for file_resolver in file_resolvers {
512 match file_resolver.resolve_binary(id) {
513 Ok(file) => return Ok(file.into_owned()),
514 Err(error) => last_error = error,
515 }
516 }
517 Err(last_error)
518 }
519
520 fn font(&self, id: usize) -> Option<Font> {
521 self.fonts[id].get()
522 }
523
524 fn today(&self, offset: Option<i64>) -> Option<Datetime> {
525 let mut now = self.now;
526 if let Some(offset) = offset {
527 now += Duration::hours(offset);
528 }
529 let date = now.date_naive();
530 let year = date.year();
531 let month = (date.month0() + 1) as u8;
532 let day = (date.day0() + 1) as u8;
533 Datetime::from_ymd(year, month, day)
534 }
535}
536
537#[derive(Debug, Clone)]
538struct InjectLocation {
539 module_name: &'static str,
540 value_name: &'static str,
541}
542
543#[derive(Debug, Clone, Error)]
544pub enum TypstAsLibError {
545 #[error("Typst source error: {0:?}")]
546 TypstSource(EcoVec<SourceDiagnostic>),
547 #[error("Typst file error: {0}")]
548 TypstFile(#[from] FileError),
549 #[error("Source file does not exist in collection: {0:?}")]
550 MainSourceFileDoesNotExist(FileId),
551 #[error("Typst hinted String: {0:?}")]
552 HintedString(HintedString),
553 #[error("Unspecified: {0}!")]
554 Unspecified(ecow::EcoString),
555}
556
557impl From<HintedString> for TypstAsLibError {
558 fn from(value: HintedString) -> Self {
559 TypstAsLibError::HintedString(value)
560 }
561}
562
563impl From<EcoVec<SourceDiagnostic>> for TypstAsLibError {
564 fn from(value: EcoVec<SourceDiagnostic>) -> Self {
565 TypstAsLibError::TypstSource(value)
566 }
567}
568
569#[derive(Debug)]
570pub enum FontEnum {
571 Font(Font),
572 #[cfg(feature = "typst-kit-fonts")]
573 FontSlot(typst_kit::fonts::FontSlot),
574}
575
576impl FontEnum {
577 pub fn get(&self) -> Option<Font> {
578 match self {
579 FontEnum::Font(font) => Some(font.clone()),
580 #[cfg(feature = "typst-kit-fonts")]
581 FontEnum::FontSlot(font_slot) => font_slot.get(),
582 }
583 }
584}