quillmark_typst/
lib.rs

1//! # Typst Backend for Quillmark
2//!
3//! This crate provides a complete Typst backend implementation that converts Markdown
4//! documents to PDF and SVG formats via the Typst typesetting system.
5//!
6//! ## Overview
7//!
8//! The primary entry point is the [`TypstBackend`] struct, which implements the
9//! [`Backend`] trait from `quillmark-core`. Users typically interact with this backend
10//! through the high-level `Workflow` API from the `quillmark` crate.
11//!
12//! ## Features
13//!
14//! - Converts CommonMark Markdown to Typst markup
15//! - Compiles Typst documents to PDF and SVG formats
16//! - Provides template filters for YAML data transformation
17//! - Manages fonts, assets, and packages dynamically
18//! - Thread-safe for concurrent rendering
19//!
20//! ## Example Usage
21//!
22//! ```no_run
23//! use quillmark_typst::TypstBackend;
24//! use quillmark_core::{Backend, Quill, OutputFormat};
25//!
26//! let backend = TypstBackend::default();
27//! let quill = Quill::from_path("path/to/quill").unwrap();
28//!
29//! // Use with Workflow API (recommended)
30//! // let workflow = Workflow::new(Box::new(backend), quill);
31//! ```
32//! ## Modules
33//!
34//! - [`convert`] - Markdown to Typst conversion utilities
35//! - [`compile`] - Typst to PDF/SVG compilation functions
36
37pub mod compile;
38pub mod convert;
39mod error_mapping;
40mod filters;
41mod world;
42
43/// Utilities exposed for fuzzing tests.
44/// Not intended for general use.
45#[doc(hidden)]
46pub mod fuzz_utils {
47    pub use super::filters::inject_json;
48}
49
50use filters::{
51    asset_filter, content_filter, date_filter, dict_filter, lines_filter, string_filter,
52};
53use quillmark_core::{Artifact, Backend, Glue, OutputFormat, Quill, RenderError, RenderOptions};
54
55/// Typst backend implementation for Quillmark.
56pub struct TypstBackend;
57
58impl Backend for TypstBackend {
59    fn id(&self) -> &'static str {
60        "typst"
61    }
62
63    fn supported_formats(&self) -> &'static [OutputFormat] {
64        &[OutputFormat::Pdf, OutputFormat::Svg]
65    }
66
67    fn glue_type(&self) -> &'static str {
68        ".typ"
69    }
70
71    fn register_filters(&self, glue: &mut Glue) {
72        // Register basic filters (simplified for now)
73        glue.register_filter("String", string_filter);
74        glue.register_filter("Lines", lines_filter);
75        glue.register_filter("Date", date_filter);
76        glue.register_filter("Dict", dict_filter);
77        glue.register_filter("Content", content_filter);
78        glue.register_filter("Asset", asset_filter);
79    }
80
81    fn compile(
82        &self,
83        glued_content: &str,
84        quill: &Quill,
85        opts: &RenderOptions,
86    ) -> Result<Vec<Artifact>, RenderError> {
87        let format = opts.output_format.unwrap_or(OutputFormat::Pdf);
88
89        // Check if format is supported
90        if !self.supported_formats().contains(&format) {
91            return Err(RenderError::FormatNotSupported {
92                backend: self.id().to_string(),
93                format,
94            });
95        }
96
97        println!("Typst backend compiling for quill: {}", quill.name);
98
99        match format {
100            OutputFormat::Pdf => {
101                let bytes = compile::compile_to_pdf(quill, glued_content)?;
102                Ok(vec![Artifact {
103                    bytes,
104                    output_format: OutputFormat::Pdf,
105                }])
106            }
107            OutputFormat::Svg => {
108                let svg_pages = compile::compile_to_svg(quill, glued_content)?;
109                Ok(svg_pages
110                    .into_iter()
111                    .map(|bytes| Artifact {
112                        bytes,
113                        output_format: OutputFormat::Svg,
114                    })
115                    .collect())
116            }
117            OutputFormat::Txt => Err(RenderError::FormatNotSupported {
118                backend: self.id().to_string(),
119                format: OutputFormat::Txt,
120            }),
121        }
122    }
123}
124
125impl Default for TypstBackend {
126    /// Creates a new [`TypstBackend`] instance.
127    fn default() -> Self {
128        Self
129    }
130}
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_backend_info() {
137        let backend = TypstBackend::default();
138        assert_eq!(backend.id(), "typst");
139        assert_eq!(backend.glue_type(), ".typ");
140        assert!(backend.supported_formats().contains(&OutputFormat::Pdf));
141        assert!(backend.supported_formats().contains(&OutputFormat::Svg));
142    }
143}
144
145// Re-export for compatibility
146pub use TypstBackend as backend;