1use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29use typst::foundations::Dict;
30
31use crate::codegen::json_to_simple_value;
32use crate::diagnostic::CompileError;
33use crate::world::{FileSnapshot, SnapshotConfig, TypstWorld};
34
35use super::compile::{compile_with_world, CompileResult};
36use super::inputs::WithInputs;
37#[cfg(feature = "scan")]
38use super::scan::{scan_impl, ScanResult};
39
40
41pub struct Batcher<'a> {
46 root: &'a Path,
47 inputs: Option<Dict>,
48 pub(crate) preludes: Vec<String>,
49 pub(crate) postludes: Vec<String>,
50 snapshot: Option<Arc<FileSnapshot>>,
51}
52
53impl<'a> WithInputs for Batcher<'a> {
54 fn inputs_mut(&mut self) -> &mut Option<Dict> {
55 &mut self.inputs
56 }
57}
58
59impl<'a> Batcher<'a> {
60 pub fn new(root: &'a Path) -> Self {
62 Self {
63 root,
64 inputs: None,
65 preludes: Vec::new(),
66 postludes: Vec::new(),
67 snapshot: None,
68 }
69 }
70
71 pub fn for_scan(root: &'a Path) -> BatchScanner<'a> {
76 BatchScanner::new(root)
77 }
78
79 pub fn with_prelude(mut self, prelude: impl Into<String>) -> Self {
81 self.preludes.push(prelude.into());
82 self
83 }
84
85 pub fn with_postlude(mut self, postlude: impl Into<String>) -> Self {
87 self.postludes.push(postlude.into());
88 self
89 }
90
91 pub fn with_snapshot_from<P: AsRef<Path>>(self, paths: &[P]) -> Result<Self, CompileError> {
101 self.with_snapshot_from_each(paths, |_| {})
102 }
103
104 pub fn with_snapshot_from_each<P, F>(mut self, paths: &[P], on_each: F) -> Result<Self, CompileError>
112 where
113 P: AsRef<Path>,
114 F: Fn(&Path) + Sync,
115 {
116 if paths.is_empty() {
117 return Ok(self);
118 }
119
120 let path_bufs: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
121
122 let config = SnapshotConfig {
124 prelude: self.build_prelude_opt(),
125 postlude: self.build_postlude_opt(),
126 };
127
128 let snapshot = Arc::new(FileSnapshot::build_with_config(&path_bufs, self.root, &config, on_each)?);
129 self.snapshot = Some(snapshot);
130
131 Ok(self)
132 }
133
134 pub fn with_snapshot(mut self, snapshot: Arc<FileSnapshot>) -> Self {
138 self.snapshot = Some(snapshot);
139 self
140 }
141
142 pub fn snapshot(&self) -> Option<Arc<FileSnapshot>> {
146 self.snapshot.clone()
147 }
148
149 #[cfg(feature = "scan")]
172 pub fn batch_scan<P: AsRef<Path> + Sync>(
173 &self,
174 paths: &[P],
175 ) -> Result<Vec<Result<ScanResult, CompileError>>, CompileError> {
176 use rayon::prelude::*;
177
178 if paths.is_empty() {
179 return Ok(vec![]);
180 }
181
182 let snapshot = self.get_or_build_snapshot(paths)?;
183
184 let results: Vec<_> = paths
186 .par_iter()
187 .map(|path| {
188 let path = path.as_ref();
189 let world = self.build_world(path, &snapshot);
190 scan_impl(&world)
191 })
192 .collect();
193
194 Ok(results)
195 }
196
197 pub fn batch_compile<P: AsRef<Path> + Sync>(
204 &self,
205 paths: &[P],
206 ) -> Result<Vec<Result<CompileResult, CompileError>>, CompileError> {
207 self.batch_compile_each(paths, |_| {})
208 }
209
210 pub fn batch_compile_each<P, F>(
215 &self,
216 paths: &[P],
217 on_each: F,
218 ) -> Result<Vec<Result<CompileResult, CompileError>>, CompileError>
219 where
220 P: AsRef<Path> + Sync,
221 F: Fn(&Path) + Sync,
222 {
223 use rayon::prelude::*;
224
225 if paths.is_empty() {
226 return Ok(vec![]);
227 }
228
229 let snapshot = self.get_or_build_snapshot(paths)?;
230
231 let results: Vec<_> = paths
233 .par_iter()
234 .map(|path| {
235 let path = path.as_ref();
236 let world = self.build_world(path, &snapshot);
237 let result = compile_with_world(&world);
238 on_each(path);
239 result
240 })
241 .collect();
242
243 Ok(results)
244 }
245
246 pub fn batch_compile_with_context<P, F>(
275 &self,
276 paths: &[P],
277 context_fn: F,
278 ) -> Result<Vec<Result<CompileResult, CompileError>>, CompileError>
279 where
280 P: AsRef<Path> + Sync,
281 F: Fn(&Path) -> serde_json::Value + Sync,
282 {
283 use rayon::prelude::*;
284
285 if paths.is_empty() {
286 return Ok(vec![]);
287 }
288
289 let snapshot = self.get_or_build_snapshot(paths)?;
290
291 let results: Vec<_> = paths
293 .par_iter()
294 .map(|path| {
295 let path = path.as_ref();
296 let context_json = context_fn(path);
297 let world = self.build_world_with_context(path, &snapshot, &context_json);
298 compile_with_world(&world)
299 })
300 .collect();
301
302 Ok(results)
303 }
304
305 fn build_world(&self, path: &Path, snapshot: &Arc<FileSnapshot>) -> TypstWorld {
306 let mut builder = TypstWorld::builder(path, self.root)
307 .with_snapshot(snapshot.clone())
308 .with_fonts();
309
310 if let Some(inputs) = &self.inputs {
311 builder = builder.with_inputs_dict(inputs.clone());
312 }
313
314 if let Some(prelude) = self.build_prelude_opt() {
317 builder = builder.with_prelude(&prelude);
318 }
319
320 builder.build()
321 }
322
323 fn build_prelude_opt(&self) -> Option<String> {
324 if self.preludes.is_empty() {
325 None
326 } else {
327 Some(self.preludes.join("\n"))
328 }
329 }
330
331 fn build_postlude_opt(&self) -> Option<String> {
332 if self.postludes.is_empty() {
333 None
334 } else {
335 Some(self.postludes.join("\n"))
336 }
337 }
338
339 fn get_or_build_snapshot<P: AsRef<Path>>(
340 &self,
341 paths: &[P],
342 ) -> Result<Arc<FileSnapshot>, CompileError> {
343 match &self.snapshot {
344 Some(s) => Ok(s.clone()),
345 None => {
346 let path_bufs: Vec<PathBuf> =
347 paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
348 let config = SnapshotConfig {
349 prelude: self.build_prelude_opt(),
350 postlude: self.build_postlude_opt(),
351 };
352 Ok(Arc::new(FileSnapshot::build_with_config(&path_bufs, self.root, &config, |_| {})?))
353 }
354 }
355 }
356
357 fn build_world_with_context(
358 &self,
359 path: &Path,
360 snapshot: &Arc<FileSnapshot>,
361 context_json: &serde_json::Value,
362 ) -> TypstWorld {
363 let mut merged = self.inputs.clone().unwrap_or_default();
365
366 if let Some(obj) = context_json.as_object() {
368 for (key, value) in obj {
369 if let Ok(typst_value) = json_to_simple_value(value) {
370 merged.insert(key.as_str().into(), typst_value);
371 }
372 }
373 }
374
375 let mut builder = TypstWorld::builder(path, self.root)
377 .with_snapshot(snapshot.clone())
378 .with_fonts()
379 .with_inputs_dict(merged);
380
381 if let Some(prelude) = self.build_prelude_opt() {
382 builder = builder.with_prelude(&prelude);
383 }
384
385 builder.build()
386 }
387}
388
389
390
391pub struct BatchScanner<'a> {
396 root: &'a Path,
397 inputs: Option<Dict>,
398 snapshot: Option<Arc<FileSnapshot>>,
399 prelude: Option<String>,
400}
401
402impl<'a> WithInputs for BatchScanner<'a> {
403 fn inputs_mut(&mut self) -> &mut Option<Dict> {
404 &mut self.inputs
405 }
406}
407
408impl<'a> BatchScanner<'a> {
409 pub fn new(root: &'a Path) -> Self {
411 Self {
412 root,
413 inputs: None,
414 snapshot: None,
415 prelude: None,
416 }
417 }
418
419 pub fn with_prelude(mut self, prelude: impl Into<String>) -> Self {
421 self.prelude = Some(prelude.into());
422 self
423 }
424
425 pub fn with_snapshot_from<P: AsRef<Path>>(mut self, paths: &[P]) -> Result<Self, CompileError> {
427 if paths.is_empty() {
428 return Ok(self);
429 }
430
431 let path_bufs: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
432 let config = SnapshotConfig {
433 prelude: self.prelude.clone(),
434 postlude: None,
435 };
436 let snapshot = Arc::new(FileSnapshot::build_with_config(&path_bufs, self.root, &config, |_| {})?);
437 self.snapshot = Some(snapshot);
438
439 Ok(self)
440 }
441
442 #[cfg(feature = "scan")]
444 pub fn batch_scan<P: AsRef<Path> + Sync>(
445 &self,
446 paths: &[P],
447 ) -> Result<Vec<Result<ScanResult, CompileError>>, CompileError> {
448 use rayon::prelude::*;
449
450 if paths.is_empty() {
451 return Ok(vec![]);
452 }
453
454 let snapshot = match &self.snapshot {
456 Some(s) => s.clone(),
457 None => {
458 let path_bufs: Vec<PathBuf> =
459 paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
460 let config = SnapshotConfig {
461 prelude: self.prelude.clone(),
462 postlude: None,
463 };
464 Arc::new(FileSnapshot::build_with_config(&path_bufs, self.root, &config, |_| {})?)
465 }
466 };
467
468 let results: Vec<_> = paths
470 .par_iter()
471 .map(|path| {
472 let path = path.as_ref();
473 let world = self.build_world(path, &snapshot);
474 scan_impl(&world)
475 })
476 .collect();
477
478 Ok(results)
479 }
480
481 fn build_world(&self, path: &Path, snapshot: &Arc<FileSnapshot>) -> TypstWorld {
482 let mut builder = TypstWorld::builder(path, self.root)
483 .with_snapshot(snapshot.clone())
484 .no_fonts();
485
486 if let Some(inputs) = &self.inputs {
487 builder = builder.with_inputs_dict(inputs.clone());
488 }
489
490 if let Some(prelude) = &self.prelude {
492 builder = builder.with_prelude(prelude);
493 }
494
495 builder.build()
496 }
497}