1use std::path::PathBuf;
25
26use anyhow::anyhow;
27use mdbook_preprocessor::book::{Book, BookItem, Chapter};
28use mdbook_preprocessor::errors::Result;
29use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
30use pulldown_cmark::{Event, Options, Parser};
31use serde::Deserialize;
32
33mod compiler;
34use compiler::{CompileError, Compiler};
35use typst::foundations::Bytes;
36use typst::text::{Font, FontInfo};
37
38pub struct TypstProcessorOptions {
43 pub preamble: String,
48 pub inline_preamble: Option<String>,
52 pub display_preamble: Option<String>,
56}
57
58#[derive(Debug, Clone, Deserialize)]
64#[serde(untagged)]
65enum FontsConfig {
66 Single(String),
67 Multiple(Vec<String>),
68}
69
70impl FontsConfig {
71 fn into_vec(self) -> Vec<String> {
72 match self {
73 FontsConfig::Single(s) => vec![s],
74 FontsConfig::Multiple(v) => v,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Default, Deserialize)]
81#[serde(default)]
82struct TypstMathConfig {
83 preamble: Option<String>,
84 inline_preamble: Option<String>,
85 display_preamble: Option<String>,
86 fonts: Option<FontsConfig>,
87 cache: Option<String>,
88}
89
90pub struct TypstProcessor;
106
107impl Preprocessor for TypstProcessor {
108 fn name(&self) -> &str {
109 "typst-math"
110 }
111
112 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
113 let config: TypstMathConfig = ctx
114 .config
115 .get(&format!("preprocessor.{}", self.name()))
116 .ok()
117 .flatten()
118 .unwrap_or_default();
119 let mut compiler = Compiler::new();
120
121 let opts = TypstProcessorOptions {
123 preamble: config.preamble.unwrap_or_else(|| {
124 String::from("#set page(width: auto, height: auto, margin: 0.5em)")
125 }),
126 inline_preamble: config.inline_preamble,
127 display_preamble: config.display_preamble,
128 };
129
130 let mut db = fontdb::Database::new();
131 if let Some(fonts) = config.fonts {
133 for font_path in fonts.into_vec() {
134 db.load_fonts_dir(font_path);
135 }
136 }
137 db.load_system_fonts();
139
140 for face in db.faces() {
142 let Some(info) = db.with_face_data(face.id, FontInfo::new).flatten() else {
143 eprintln!(
144 "Warning: Failed to load font info for {:?}, skipping",
145 face.source
146 );
147 continue;
148 };
149 compiler.book.push(info);
150 let font = match &face.source {
151 fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => {
152 match std::fs::read(path) {
153 Ok(bytes) => Font::new(Bytes::new(bytes), face.index),
154 Err(e) => {
155 eprintln!(
156 "Warning: Failed to read font file {:?}: {}, skipping",
157 path, e
158 );
159 continue;
160 }
161 }
162 }
163 fontdb::Source::Binary(data) => {
164 Font::new(Bytes::new(data.as_ref().as_ref().to_vec()), face.index)
165 }
166 };
167 if let Some(font) = font {
168 compiler.fonts.push(font);
169 }
170 }
171
172 #[cfg(feature = "embed-fonts")]
173 {
174 for data in typst_assets::fonts() {
176 let buffer = Bytes::new(data);
177 for font in Font::iter(buffer) {
178 compiler.book.push(font.info().clone());
179 compiler.fonts.push(font);
180 }
181 }
182 }
183
184 if let Some(ref cache) = config.cache {
186 compiler.cache = PathBuf::from(cache);
187 }
188
189 let mut res = None;
191
192 book.for_each_mut(|item| {
193 if let Some(Err(_)) = res {
194 return;
195 }
196
197 if let BookItem::Chapter(ref mut chapter) = *item {
198 res = Some(self.convert_typst(chapter, &compiler, &opts).map(|c| {
199 chapter.content = c;
200 }))
201 }
202 });
203
204 res.unwrap_or(Ok(())).map(|_| book)
205 }
206
207 fn supports_renderer(&self, renderer: &str) -> Result<bool> {
208 Ok(renderer == "html")
209 }
210}
211
212impl TypstProcessor {
213 fn convert_typst(
214 &self,
215 chapter: &Chapter,
216 compiler: &Compiler,
217 opts: &TypstProcessorOptions,
218 ) -> Result<String> {
219 let chapter_name = chapter.name.as_str();
220 let mut typst_blocks = Vec::new();
221
222 let mut pulldown_cmark_opts = Options::empty();
223 pulldown_cmark_opts.insert(Options::ENABLE_TABLES);
224 pulldown_cmark_opts.insert(Options::ENABLE_FOOTNOTES);
225 pulldown_cmark_opts.insert(Options::ENABLE_STRIKETHROUGH);
226 pulldown_cmark_opts.insert(Options::ENABLE_TASKLISTS);
227 pulldown_cmark_opts.insert(Options::ENABLE_MATH);
228
229 let parser = Parser::new_ext(&chapter.content, pulldown_cmark_opts);
230 for (e, span) in parser.into_offset_iter() {
231 if let Event::InlineMath(math_content) = e {
232 typst_blocks.push((
233 span,
234 format!(
235 "{}\n${math_content}$",
236 opts.inline_preamble.as_ref().unwrap_or(&opts.preamble)
237 ),
238 true,
239 ))
240 } else if let Event::DisplayMath(math_content) = e {
241 let math_content = math_content.trim();
242 typst_blocks.push((
243 span,
244 format!(
245 "{}\n$ {math_content} $",
246 opts.display_preamble.as_ref().unwrap_or(&opts.preamble)
247 ),
248 false,
249 ))
250 }
251 }
252
253 let mut content = chapter.content.to_string();
254
255 for (span, block, inline) in typst_blocks.iter().rev() {
256 let pre_content = &content[0..span.start];
257 let post_content = &content[span.end..];
258
259 let svg = compiler.render(block.clone()).map_err(|e: CompileError| {
260 anyhow!("Failed to render math in chapter '{}': {}", chapter_name, e)
261 })?;
262
263 content = match inline {
264 true => format!(
265 "{}<span class=\"typst-inline\">{}</span>{}",
266 pre_content, svg, post_content
267 ),
268 false => format!(
269 "{}<div class=\"typst-display\">{}</div>{}",
270 pre_content, svg, post_content
271 ),
272 };
273 }
274
275 Ok(content)
276 }
277}