typst_batch/compile.rs
1//! High-level compilation API for Typst to HTML.
2//!
3//! This module provides convenient functions for batch compilation workflows.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use typst_batch::{compile_html, get_fonts};
9//! use std::path::Path;
10//!
11//! // Initialize fonts once at startup
12//! get_fonts(&[]);
13//!
14//! // Compile a file to HTML
15//! let result = compile_html(Path::new("doc.typ"), Path::new("."))?;
16//! println!("HTML: {} bytes", result.html.len());
17//!
18//! // Access diagnostics (warnings)
19//! for diag in &result.diagnostics {
20//! println!("Warning: {}", diag.message);
21//! }
22//!
23//! // With metadata extraction
24//! let result = compile_html_with_metadata(
25//! Path::new("doc.typ"),
26//! Path::new("."),
27//! "my-meta", // label name in typst: #metadata(...) <my-meta>
28//! )?;
29//! if let Some(meta) = result.metadata {
30//! println!("Title: {:?}", meta.get("title"));
31//! }
32//! ```
33//!
34//! # sys.inputs Support
35//!
36//! For documents that need `sys.inputs`, use [`compile_html_with_inputs`]:
37//!
38//! ```ignore
39//! let result = compile_html_with_inputs(
40//! Path::new("doc.typ"),
41//! Path::new("."),
42//! [("title", "Hello"), ("author", "Alice")],
43//! )?;
44//! ```
45
46use std::path::{Path, PathBuf};
47
48use serde_json::Value as JsonValue;
49use typst::diag::SourceDiagnostic;
50use typst::foundations::{Dict, Label, Selector};
51use typst::introspection::MetadataElem;
52use typst::utils::PicoStr;
53use typst::Document;
54use typst_html::HtmlDocument;
55
56use crate::diagnostic::{filter_html_warnings, has_errors, CompileError};
57use crate::file::{get_accessed_files, reset_access_flags};
58use crate::world::SystemWorld;
59
60// =============================================================================
61// Result Types
62// =============================================================================
63
64/// Result of HTML compilation.
65#[derive(Debug)]
66pub struct HtmlResult {
67 /// Compiled HTML content as bytes.
68 pub html: Vec<u8>,
69 /// Files accessed during compilation (relative to root).
70 pub accessed_files: Vec<PathBuf>,
71 /// Compilation diagnostics (warnings only - errors cause Err return).
72 ///
73 /// Use [`crate::DiagnosticsExt::format`] to format for display.
74 pub diagnostics: Vec<SourceDiagnostic>,
75}
76
77/// Result of HTML compilation with metadata extraction.
78#[derive(Debug)]
79pub struct HtmlWithMetadataResult {
80 /// Compiled HTML content as bytes.
81 pub html: Vec<u8>,
82 /// Extracted metadata as JSON (if label found).
83 pub metadata: Option<JsonValue>,
84 /// Files accessed during compilation (relative to root).
85 pub accessed_files: Vec<PathBuf>,
86 /// Compilation diagnostics (warnings only - errors cause Err return).
87 pub diagnostics: Vec<SourceDiagnostic>,
88}
89
90/// Result of document compilation (without HTML serialization).
91#[derive(Debug)]
92pub struct DocumentResult {
93 /// The compiled HTML document (for further processing).
94 pub document: HtmlDocument,
95 /// Files accessed during compilation (relative to root).
96 pub accessed_files: Vec<PathBuf>,
97 /// Compilation diagnostics (warnings only - errors cause Err return).
98 pub diagnostics: Vec<SourceDiagnostic>,
99}
100
101/// Result of document compilation with metadata.
102#[derive(Debug)]
103pub struct DocumentWithMetadataResult {
104 /// The compiled HTML document (for further processing).
105 pub document: HtmlDocument,
106 /// Extracted metadata as JSON (if label found).
107 pub metadata: Option<JsonValue>,
108 /// Files accessed during compilation (relative to root).
109 pub accessed_files: Vec<PathBuf>,
110 /// Compilation diagnostics (warnings only - errors cause Err return).
111 pub diagnostics: Vec<SourceDiagnostic>,
112}
113
114// =============================================================================
115// Compilation Functions
116// =============================================================================
117
118/// Compile a Typst file to HTML bytes.
119///
120/// This is the simplest API for getting HTML output from a Typst file.
121///
122/// # Arguments
123///
124/// * `path` - Path to the .typ file to compile
125/// * `root` - Project root directory (for resolving imports)
126///
127/// # Returns
128///
129/// Returns `HtmlResult` containing the HTML bytes and accessed files.
130///
131/// # Example
132///
133/// ```ignore
134/// let result = compile_html(Path::new("doc.typ"), Path::new("."))?;
135/// std::fs::write("output.html", &result.html)?;
136/// ```
137pub fn compile_html(path: &Path, root: &Path) -> Result<HtmlResult, CompileError> {
138 let (document, accessed_files, diagnostics) = compile_document_internal(path, root)?;
139
140 let html = typst_html::html(&document)
141 .map_err(|e| CompileError::html_export(format!("{e:?}")))?
142 .into_bytes();
143
144 Ok(HtmlResult {
145 html,
146 accessed_files,
147 diagnostics,
148 })
149}
150
151/// Compile a Typst file to HTML bytes with metadata extraction.
152///
153/// Extracts metadata from a labeled metadata element in the document.
154/// In your Typst file, use: `#metadata((...)) <label-name>`
155///
156/// # Arguments
157///
158/// * `path` - Path to the .typ file to compile
159/// * `root` - Project root directory
160/// * `label` - The label name to query (without angle brackets)
161///
162/// # Example
163///
164/// ```ignore
165/// // In your .typ file:
166/// // #metadata((title: "My Post", date: "2024-01-01")) <post-meta>
167///
168/// let result = compile_html_with_metadata(
169/// Path::new("post.typ"),
170/// Path::new("."),
171/// "post-meta",
172/// )?;
173/// ```
174pub fn compile_html_with_metadata(
175 path: &Path,
176 root: &Path,
177 label: &str,
178) -> Result<HtmlWithMetadataResult, CompileError> {
179 let (document, accessed_files, diagnostics) = compile_document_internal(path, root)?;
180
181 let metadata = query_metadata(&document, label);
182
183 let html = typst_html::html(&document)
184 .map_err(|e| CompileError::html_export(format!("{e:?}")))?
185 .into_bytes();
186
187 Ok(HtmlWithMetadataResult {
188 html,
189 metadata,
190 accessed_files,
191 diagnostics,
192 })
193}
194
195/// Compile a Typst file to HtmlDocument (without serializing to bytes).
196///
197/// Use this when you need to process the document further (e.g., with tola-vdom).
198///
199/// # Arguments
200///
201/// * `path` - Path to the .typ file to compile
202/// * `root` - Project root directory
203pub fn compile_document(path: &Path, root: &Path) -> Result<DocumentResult, CompileError> {
204 let (document, accessed_files, diagnostics) = compile_document_internal(path, root)?;
205
206 Ok(DocumentResult {
207 document,
208 accessed_files,
209 diagnostics,
210 })
211}
212
213/// Compile a Typst file to HtmlDocument with metadata extraction.
214///
215/// Use this when you need both the document for further processing and metadata.
216pub fn compile_document_with_metadata(
217 path: &Path,
218 root: &Path,
219 label: &str,
220) -> Result<DocumentWithMetadataResult, CompileError> {
221 let (document, accessed_files, diagnostics) = compile_document_internal(path, root)?;
222
223 let metadata = query_metadata(&document, label);
224
225 Ok(DocumentWithMetadataResult {
226 document,
227 metadata,
228 accessed_files,
229 diagnostics,
230 })
231}
232
233// =============================================================================
234// Metadata Query
235// =============================================================================
236
237/// Query metadata from a compiled document by label name.
238///
239/// In Typst, you can attach metadata to a label like this:
240/// ```typst
241/// #metadata((title: "Hello", author: "Alice")) <my-meta>
242/// ```
243///
244/// Then query it:
245/// ```ignore
246/// let meta = query_metadata(&document, "my-meta");
247/// // Returns: Some({"title": "Hello", "author": "Alice"})
248/// ```
249///
250/// # Arguments
251///
252/// * `document` - The compiled HtmlDocument
253/// * `label` - The label name (without angle brackets)
254///
255/// # Returns
256///
257/// Returns `Some(JsonValue)` if the label exists and contains valid metadata,
258/// `None` otherwise.
259pub fn query_metadata(document: &HtmlDocument, label: &str) -> Option<JsonValue> {
260 let label = Label::new(PicoStr::intern(label))?;
261 let introspector = document.introspector();
262 let elem = introspector.query_unique(&Selector::Label(label)).ok()?;
263
264 elem.to_packed::<MetadataElem>()
265 .and_then(|meta| serde_json::to_value(&meta.value).ok())
266}
267
268/// Query multiple metadata labels from a compiled document.
269///
270/// This is useful when you have multiple metadata elements in a document.
271///
272/// # Example
273///
274/// ```typst
275/// #metadata((title: "My Post")) <post-meta>
276/// #metadata((author: "Alice", bio: "...")) <author-meta>
277/// ```
278///
279/// ```ignore
280/// let meta = query_metadata_map(&document, &["post-meta", "author-meta"]);
281/// // Returns: {"post-meta": {"title": "My Post"}, "author-meta": {"author": "Alice", ...}}
282/// ```
283///
284/// # Arguments
285///
286/// * `document` - The compiled HtmlDocument
287/// * `labels` - Slice of label names to query
288///
289/// # Returns
290///
291/// Returns a map from label name to metadata value. Labels not found are omitted.
292pub fn query_metadata_map<'a>(
293 document: &HtmlDocument,
294 labels: impl IntoIterator<Item = &'a str>,
295) -> serde_json::Map<String, JsonValue> {
296 let mut result = serde_json::Map::new();
297
298 for label in labels {
299 if let Some(value) = query_metadata(document, label) {
300 result.insert(label.to_string(), value);
301 }
302 }
303
304 result
305}
306
307// =============================================================================
308// Compilation with sys.inputs
309// =============================================================================
310
311/// Compile a Typst file to HTML bytes with custom `sys.inputs`.
312///
313/// This allows passing document-specific data accessible via `sys.inputs`
314/// in the Typst document.
315///
316/// # Arguments
317///
318/// * `path` - Path to the .typ file to compile
319/// * `root` - Project root directory
320/// * `inputs` - Key-value pairs accessible as `sys.inputs`
321///
322/// # Example
323///
324/// ```ignore
325/// let result = compile_html_with_inputs(
326/// Path::new("doc.typ"),
327/// Path::new("."),
328/// [("title", "Hello"), ("author", "Alice")],
329/// )?;
330/// ```
331///
332/// In your Typst document:
333/// ```typst
334/// #let title = sys.inputs.at("title", default: "Untitled")
335/// = #title
336/// ```
337///
338/// # Performance Note
339///
340/// This creates a new library instance per compilation. For batch compilation
341/// without inputs, use [`compile_html`] which shares the global library.
342pub fn compile_html_with_inputs<I, K, V>(
343 path: &Path,
344 root: &Path,
345 inputs: I,
346) -> Result<HtmlResult, CompileError>
347where
348 I: IntoIterator<Item = (K, V)>,
349 K: Into<typst::foundations::Str>,
350 V: typst::foundations::IntoValue,
351{
352 let (document, accessed_files, diagnostics) =
353 compile_document_internal_with_inputs(path, root, inputs)?;
354
355 let html = typst_html::html(&document)
356 .map_err(|e| CompileError::html_export(format!("{e:?}")))?
357 .into_bytes();
358
359 Ok(HtmlResult {
360 html,
361 accessed_files,
362 diagnostics,
363 })
364}
365
366/// Compile a Typst file to HTML bytes with custom `sys.inputs` from a `Dict`.
367///
368/// This is useful when you already have a pre-built `Dict` of inputs.
369pub fn compile_html_with_inputs_dict(
370 path: &Path,
371 root: &Path,
372 inputs: Dict,
373) -> Result<HtmlResult, CompileError> {
374 let (document, accessed_files, diagnostics) =
375 compile_document_internal_with_inputs_dict(path, root, inputs)?;
376
377 let html = typst_html::html(&document)
378 .map_err(|e| CompileError::html_export(format!("{e:?}")))?
379 .into_bytes();
380
381 Ok(HtmlResult {
382 html,
383 accessed_files,
384 diagnostics,
385 })
386}
387
388/// Compile to HtmlDocument with custom `sys.inputs`.
389pub fn compile_document_with_inputs<I, K, V>(
390 path: &Path,
391 root: &Path,
392 inputs: I,
393) -> Result<DocumentResult, CompileError>
394where
395 I: IntoIterator<Item = (K, V)>,
396 K: Into<typst::foundations::Str>,
397 V: typst::foundations::IntoValue,
398{
399 let (document, accessed_files, diagnostics) =
400 compile_document_internal_with_inputs(path, root, inputs)?;
401
402 Ok(DocumentResult {
403 document,
404 accessed_files,
405 diagnostics,
406 })
407}
408
409// =============================================================================
410// Internal Helpers
411// =============================================================================
412
413/// Core compilation logic.
414fn compile_document_internal(
415 path: &Path,
416 root: &Path,
417) -> Result<(HtmlDocument, Vec<PathBuf>, Vec<SourceDiagnostic>), CompileError> {
418 let world = SystemWorld::new(path, root);
419 compile_with_world(&world)
420}
421
422/// Core compilation logic with custom inputs.
423fn compile_document_internal_with_inputs<I, K, V>(
424 path: &Path,
425 root: &Path,
426 inputs: I,
427) -> Result<(HtmlDocument, Vec<PathBuf>, Vec<SourceDiagnostic>), CompileError>
428where
429 I: IntoIterator<Item = (K, V)>,
430 K: Into<typst::foundations::Str>,
431 V: typst::foundations::IntoValue,
432{
433 let world = SystemWorld::new(path, root).with_inputs(inputs);
434 compile_with_world(&world)
435}
436
437/// Core compilation logic with Dict inputs.
438fn compile_document_internal_with_inputs_dict(
439 path: &Path,
440 root: &Path,
441 inputs: Dict,
442) -> Result<(HtmlDocument, Vec<PathBuf>, Vec<SourceDiagnostic>), CompileError> {
443 let world = SystemWorld::new(path, root).with_inputs_dict(inputs);
444 compile_with_world(&world)
445}
446
447/// Shared compilation logic.
448fn compile_with_world(
449 world: &SystemWorld,
450) -> Result<(HtmlDocument, Vec<PathBuf>, Vec<SourceDiagnostic>), CompileError> {
451 reset_access_flags();
452
453 let result = typst::compile(world);
454
455 // Check for errors in warnings (shouldn't happen, but handle it)
456 if has_errors(&result.warnings) {
457 return Err(CompileError::compilation(world, result.warnings.to_vec()));
458 }
459
460 // Extract document or return errors
461 let document = result.output.map_err(|errors| {
462 let all_diags: Vec<_> = errors.iter().chain(&result.warnings).cloned().collect();
463 let filtered = filter_html_warnings(&all_diags);
464 CompileError::compilation(world, filtered)
465 })?;
466
467 // Collect accessed files
468 let accessed_files = collect_accessed_files(world.root());
469
470 // Filter HTML development warnings
471 let diagnostics = filter_html_warnings(&result.warnings);
472
473 Ok((document, accessed_files, diagnostics))
474}
475
476/// Collect accessed files relative to root.
477fn collect_accessed_files(root: &Path) -> Vec<PathBuf> {
478 get_accessed_files()
479 .into_iter()
480 .filter(|id| id.package().is_none()) // Skip package files
481 .filter_map(|id| id.vpath().resolve(root))
482 .collect()
483}
484
485#[cfg(test)]
486mod tests {
487 #[test]
488 fn test_query_metadata_not_found() {
489 // This would require a compiled document, skip for now
490 }
491}