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}