typst_batch/process/
compile.rs1use std::path::{Path, PathBuf};
40
41use typst::foundations::Dict;
42
43use crate::diagnostic::{filter_html_warnings, has_errors, CompileError, Diagnostics};
44use crate::html::HtmlDocument;
45use crate::world::TypstWorld;
46
47use super::inputs::WithInputs;
48use super::session::{AccessedDeps, CompileSession};
49use crate::resource::file::PackageId;
50
51type WorldBuilderFn<'a> = Box<dyn FnOnce(MainPath<'_>, RootPath<'_>) -> TypstWorld + 'a>;
53
54
55
56pub struct MainPath<'a>(&'a Path);
61
62impl<'a> MainPath<'a> {
63 pub fn as_path(&self) -> &'a Path {
65 self.0
66 }
67}
68
69impl AsRef<Path> for MainPath<'_> {
70 fn as_ref(&self) -> &Path {
71 self.0
72 }
73}
74
75pub struct RootPath<'a>(&'a Path);
80
81impl<'a> RootPath<'a> {
82 pub fn as_path(&self) -> &'a Path {
84 self.0
85 }
86}
87
88impl AsRef<Path> for RootPath<'_> {
89 fn as_ref(&self) -> &Path {
90 self.0
91 }
92}
93
94
95
96pub struct Compiler<'a> {
119 root: &'a Path,
120 inputs: Option<Dict>,
121 preludes: Vec<String>,
122 postludes: Vec<String>,
123}
124
125impl<'a> WithInputs for Compiler<'a> {
126 fn inputs_mut(&mut self) -> &mut Option<Dict> {
127 &mut self.inputs
128 }
129}
130
131impl<'a> Compiler<'a> {
132 pub fn new(root: &'a Path) -> Self {
134 Self {
135 root,
136 inputs: None,
137 preludes: Vec::new(),
138 postludes: Vec::new(),
139 }
140 }
141
142 pub fn with_prelude(mut self, prelude: impl Into<String>) -> Self {
144 self.preludes.push(prelude.into());
145 self
146 }
147
148 pub fn with_postlude(mut self, postlude: impl Into<String>) -> Self {
150 self.postludes.push(postlude.into());
151 self
152 }
153
154 pub fn with_path<P: AsRef<Path>>(self, path: P) -> SingleCompiler<'a> {
156 SingleCompiler {
157 root: self.root,
158 path: path.as_ref().to_path_buf(),
159 inputs: self.inputs,
160 preludes: self.preludes,
161 postludes: self.postludes,
162 world_builder: None,
163 }
164 }
165
166 #[cfg(feature = "batch")]
174 pub fn into_batch(self) -> super::batch::Batcher<'a> {
175 let mut batcher = super::batch::Batcher::new(self.root);
176 if let Some(inputs) = self.inputs {
177 batcher = batcher.with_inputs_dict(inputs);
178 }
179 batcher.preludes = self.preludes;
180 batcher.postludes = self.postludes;
181 batcher
182 }
183}
184
185
186
187pub struct SingleCompiler<'a> {
208 root: &'a Path,
209 path: PathBuf,
210 inputs: Option<Dict>,
211 preludes: Vec<String>,
212 postludes: Vec<String>,
213 world_builder: Option<WorldBuilderFn<'a>>,
214}
215
216impl<'a> WithInputs for SingleCompiler<'a> {
217 fn inputs_mut(&mut self) -> &mut Option<Dict> {
218 &mut self.inputs
219 }
220}
221
222impl<'a> SingleCompiler<'a> {
223 pub fn with_prelude(mut self, prelude: impl Into<String>) -> Self {
225 self.preludes.push(prelude.into());
226 self
227 }
228
229 pub fn with_postlude(mut self, postlude: impl Into<String>) -> Self {
231 self.postludes.push(postlude.into());
232 self
233 }
234
235 pub fn with_world<F>(mut self, f: F) -> Self
254 where
255 F: FnOnce(MainPath<'_>, RootPath<'_>) -> TypstWorld + 'a,
256 {
257 self.world_builder = Some(Box::new(f));
258 self
259 }
260
261 pub fn compile(self) -> Result<CompileResult, CompileError> {
263 let world = match self.world_builder {
264 Some(builder) => builder(MainPath(&self.path), RootPath(self.root)),
265 None => self.default_world(),
266 };
267 compile_with_world(&world)
268 }
269
270 fn default_world(&self) -> TypstWorld {
271 let mut builder = TypstWorld::builder(&self.path, self.root)
272 .with_shared_cache()
273 .with_fonts();
274
275 if let Some(inputs) = &self.inputs {
276 builder = builder.with_inputs_dict(inputs.clone());
277 }
278
279 let combined_prelude = self.build_prelude();
281 if !combined_prelude.is_empty() {
282 builder = builder.with_prelude(combined_prelude);
283 }
284
285 let combined_postlude = self.postludes.join("\n");
287 if !combined_postlude.is_empty() {
288 builder = builder.with_postlude(combined_postlude);
289 }
290
291 builder.build()
292 }
293
294 fn build_prelude(&self) -> String {
295 self.preludes.join("\n")
296 }
297}
298
299
300
301#[derive(Debug)]
303pub struct CompileResult {
304 document: HtmlDocument,
305 accessed: AccessedDeps,
306 diagnostics: Diagnostics,
307}
308
309impl CompileResult {
310 pub fn document(&self) -> &HtmlDocument {
312 &self.document
313 }
314
315 pub fn html(&self) -> Result<Vec<u8>, CompileError> {
317 typst_html::html(self.document.as_inner())
318 .map(|s| s.into_bytes())
319 .map_err(|e| CompileError::html_export(format!("{e:?}")))
320 }
321
322 pub fn accessed(&self) -> &AccessedDeps {
324 &self.accessed
325 }
326
327 pub fn accessed_files(&self) -> &[PathBuf] {
329 &self.accessed.files
330 }
331
332 pub fn accessed_packages(&self) -> &[PackageId] {
336 &self.accessed.packages
337 }
338
339 pub fn diagnostics(&self) -> &Diagnostics {
341 &self.diagnostics
342 }
343
344 pub fn into_document(self) -> HtmlDocument {
346 self.document
347 }
348
349 pub fn into_parts(self) -> (HtmlDocument, AccessedDeps, Diagnostics) {
351 (self.document, self.accessed, self.diagnostics)
352 }
353}
354
355
356
357pub(crate) fn compile_with_world(world: &TypstWorld) -> Result<CompileResult, CompileError> {
358 let session = CompileSession::start();
359 let line_offset = world.prelude_line_count();
360
361 let result = typst::compile(world);
362
363 if has_errors(&result.warnings) {
364 return Err(CompileError::compilation_with_offset(world, result.warnings.to_vec(), line_offset));
365 }
366
367 let document = result.output.map_err(|errors| {
368 let all_diags: Vec<_> = errors.iter().chain(&result.warnings).cloned().collect();
369 let filtered = filter_html_warnings(&all_diags);
370 CompileError::compilation_with_offset(world, filtered, line_offset)
371 })?;
372
373 let document = HtmlDocument::new(document);
374
375 let accessed = session.finish(world.root());
376 let filtered_warnings = filter_html_warnings(&result.warnings);
377 let diagnostics = Diagnostics::resolve_with_offset(world, &filtered_warnings, line_offset);
378
379 Ok(CompileResult {
380 document,
381 accessed,
382 diagnostics,
383 })
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use std::fs;
390 use tempfile::TempDir;
391
392 #[test]
393 fn test_simple_compile() {
394 let dir = TempDir::new().unwrap();
395 let file = dir.path().join("test.typ");
396 fs::write(&file, "= Hello World").unwrap();
397
398 let result = Compiler::new(dir.path()).with_path(&file).compile();
399 assert!(result.is_ok());
400
401 let html = result.unwrap().html().unwrap();
402 assert!(String::from_utf8_lossy(&html).contains("Hello World"));
403 }
404
405 #[test]
406 fn test_compile_with_inputs() {
407 let dir = TempDir::new().unwrap();
408 let file = dir.path().join("test.typ");
409 fs::write(
410 &file,
411 r#"#let title = sys.inputs.at("title", default: "Default")
412= #title"#,
413 )
414 .unwrap();
415
416 let result = Compiler::new(dir.path())
417 .with_inputs([("title", "Custom Title")])
418 .with_path(&file)
419 .compile();
420
421 assert!(result.is_ok());
422 let html = result.unwrap().html().unwrap();
423 assert!(String::from_utf8_lossy(&html).contains("Custom Title"));
424 }
425
426 #[test]
427 fn test_query_metadata() {
428 let dir = TempDir::new().unwrap();
429 let file = dir.path().join("test.typ");
430 fs::write(
431 &file,
432 r#"#metadata((title: "Test", draft: false)) <post-meta>
433= Content"#,
434 )
435 .unwrap();
436
437 let result = Compiler::new(dir.path())
438 .with_path(&file)
439 .compile()
440 .unwrap();
441 let meta = result.document().query_metadata("post-meta");
442
443 assert!(meta.is_some());
444 let meta = meta.unwrap();
445 assert_eq!(
446 meta.get("title")
447 .and_then(|v: &serde_json::Value| v.as_str()),
448 Some("Test")
449 );
450 }
451
452 #[test]
453 fn test_query_metadata_all() {
454 let dir = TempDir::new().unwrap();
455 let file = dir.path().join("test.typ");
456 fs::write(
457 &file,
458 r#"#metadata((id: 1, role: "first")) <item>
459#metadata((id: 2, role: "second")) <item>
460= Content"#,
461 )
462 .unwrap();
463
464 let result = Compiler::new(dir.path())
465 .with_path(&file)
466 .compile()
467 .unwrap();
468 let metas = result.document().query_metadata_all("item");
469
470 assert_eq!(metas.len(), 2);
471 assert_eq!(metas[0].get("id").and_then(|v| v.as_i64()), Some(1));
472 assert_eq!(metas[1].get("id").and_then(|v| v.as_i64()), Some(2));
473 }
474
475 #[test]
476 #[cfg(feature = "batch")]
477 fn test_batch_compile() {
478 let dir = TempDir::new().unwrap();
479
480 let file1 = dir.path().join("test1.typ");
481 let file2 = dir.path().join("test2.typ");
482 fs::write(&file1, "= File One").unwrap();
483 fs::write(&file2, "= File Two").unwrap();
484
485 let results = Compiler::new(dir.path())
486 .into_batch()
487 .batch_compile(&[&file1, &file2])
488 .unwrap();
489
490 assert_eq!(results.len(), 2);
491 assert!(results[0].is_ok());
492 assert!(results[1].is_ok());
493
494 let html1 = results[0].as_ref().unwrap().html().unwrap();
495 let html2 = results[1].as_ref().unwrap().html().unwrap();
496 assert!(String::from_utf8_lossy(&html1).contains("File One"));
497 assert!(String::from_utf8_lossy(&html2).contains("File Two"));
498 }
499
500 #[test]
501 #[cfg(feature = "batch")]
502 fn test_batch_with_snapshot_from() {
503 let dir = TempDir::new().unwrap();
504
505 let file1 = dir.path().join("test1.typ");
506 let file2 = dir.path().join("test2.typ");
507 fs::write(&file1, "= File One").unwrap();
508 fs::write(&file2, "= File Two").unwrap();
509
510 let batch = Compiler::new(dir.path())
511 .into_batch()
512 .with_snapshot_from(&[&file1, &file2])
513 .unwrap();
514
515 let results1 = batch.batch_compile(&[&file1, &file2]).unwrap();
517 assert_eq!(results1.len(), 2);
518
519 let results2 = batch.batch_compile(&[&file1]).unwrap();
521 assert_eq!(results2.len(), 1);
522 }
523}