1use std::path::PathBuf;
2
3use anyhow::anyhow;
4use mdbook::book::{Book, Chapter};
5use mdbook::errors::Result;
6use mdbook::preprocess::{Preprocessor, PreprocessorContext};
7use mdbook::BookItem;
8use pulldown_cmark::{Event, Options, Parser};
9
10mod compiler;
11use compiler::Compiler;
12use typst::foundations::Bytes;
13use typst::text::{Font, FontInfo};
14
15pub struct TypstProcessorOptions {
17 pub preamble: String,
21 pub inline_preamble: Option<String>,
23 pub display_preamble: Option<String>,
25}
26
27pub struct TypstProcessor;
28
29impl Preprocessor for TypstProcessor {
30 fn name(&self) -> &str {
31 "typst-math"
32 }
33
34 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
35 let config = ctx.config.get_preprocessor(self.name());
36 let mut compiler = Compiler::new();
37
38 let mut opts = TypstProcessorOptions {
40 preamble: String::from("#set page(width: auto, height: auto, margin: 0.5em)"),
41 inline_preamble: None,
42 display_preamble: None,
43 };
44 if let Some(preamble) = config.and_then(|c| c.get("preamble")) {
45 opts.preamble = preamble
46 .as_str()
47 .map(String::from)
48 .expect("preamble must be a string");
49 }
50 if let Some(inline_preamble) = config.and_then(|c| c.get("inline_preamble")) {
51 opts.inline_preamble = Some(
52 inline_preamble
53 .as_str()
54 .map(String::from)
55 .expect("inline_preamble must be a string"),
56 );
57 }
58 if let Some(display_preamble) = config.and_then(|c| c.get("display_preamble")) {
59 opts.display_preamble = Some(
60 display_preamble
61 .as_str()
62 .map(String::from)
63 .expect("display_preamble must be a string"),
64 );
65 }
66
67 let mut db = fontdb::Database::new();
68 if let Some(fonts) = config.and_then(|c| c.get("fonts")) {
70 if let Some(fonts) = fonts.as_array() {
71 for font in fonts {
72 let font = font.as_str().unwrap();
73 db.load_fonts_dir(font);
74 }
75 };
76 if let Some(font) = fonts.as_str() {
77 db.load_fonts_dir(font);
78 };
79 }
80 db.load_system_fonts();
82
83 for face in db.faces() {
85 let info = db
86 .with_face_data(face.id, FontInfo::new)
87 .expect("Failed to load font info");
88 if let Some(info) = info {
89 compiler.book.update(|book| book.push(info));
90 if let Some(font) = match &face.source {
91 fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => {
92 let bytes = std::fs::read(path).expect("Failed to read font file");
93 Font::new(Bytes::from(bytes), face.index)
94 }
95 fontdb::Source::Binary(data) => {
96 Font::new(Bytes::from(data.as_ref().as_ref()), face.index)
97 }
98 } {
99 compiler.fonts.push(font);
100 }
101 }
102 }
103
104 #[cfg(feature = "embed-fonts")]
105 {
106 for data in typst_assets::fonts() {
108 let buffer = Bytes::from_static(data);
109 for font in Font::iter(buffer) {
110 compiler.book.update(|book| book.push(font.info().clone()));
111 compiler.fonts.push(font);
112 }
113 }
114 }
115
116 if let Some(cache) = config.and_then(|c| c.get("cache")) {
118 compiler.cache = cache
119 .as_str()
120 .map(PathBuf::from)
121 .expect("cache dir must be a string");
122 }
123
124 let mut res = None;
126
127 book.for_each_mut(|item| {
128 if let Some(Err(_)) = res {
129 return;
130 }
131
132 if let BookItem::Chapter(ref mut chapter) = *item {
133 res = Some(self.convert_typst(chapter, &compiler, &opts).map(|c| {
134 chapter.content = c;
135 }))
136 }
137 });
138
139 res.unwrap_or(Ok(())).map(|_| book)
140 }
141
142 fn supports_renderer(&self, renderer: &str) -> bool {
143 renderer == "html"
144 }
145}
146
147impl TypstProcessor {
148 fn convert_typst(
149 &self,
150 chapter: &mut Chapter,
151 compiler: &Compiler,
152 opts: &TypstProcessorOptions,
153 ) -> Result<String> {
154 let mut typst_blocks = Vec::new();
155
156 let mut pulldown_cmark_opts = Options::empty();
157 pulldown_cmark_opts.insert(Options::ENABLE_TABLES);
158 pulldown_cmark_opts.insert(Options::ENABLE_FOOTNOTES);
159 pulldown_cmark_opts.insert(Options::ENABLE_STRIKETHROUGH);
160 pulldown_cmark_opts.insert(Options::ENABLE_TASKLISTS);
161 pulldown_cmark_opts.insert(Options::ENABLE_MATH);
162
163 let parser = Parser::new_ext(&chapter.content, pulldown_cmark_opts);
164 for (e, span) in parser.into_offset_iter() {
165 if let Event::InlineMath(math_content) = e {
166 typst_blocks.push((
167 span,
168 format!(
169 "{}\n${math_content}$",
170 opts.inline_preamble.as_ref().unwrap_or(&opts.preamble)
171 ),
172 true,
173 ))
174 } else if let Event::DisplayMath(math_content) = e {
175 let math_content = math_content.trim();
176 typst_blocks.push((
177 span,
178 format!(
179 "{}\n$ {math_content} $",
180 opts.display_preamble.as_ref().unwrap_or(&opts.preamble)
181 ),
182 false,
183 ))
184 }
185 }
186
187 let mut content = chapter.content.to_string();
188
189 for (span, block, inline) in typst_blocks.iter().rev() {
190 let pre_content = &content[0..span.start];
191 let post_content = &content[span.end..];
192
193 let svg = compiler.render(block.clone()).map_err(|e| anyhow!(e))?;
194
195 content = match inline {
196 true => format!(
197 "{}<span class=\"typst-inline\">{}</span>{}",
198 pre_content, svg, post_content
199 ),
200 false => format!(
201 "{}<div class=\"typst-display\">{}</div>{}",
202 pre_content, svg, post_content
203 ),
204 };
205 }
206
207 Ok(content)
208 }
209}