1mod font;
142
143#[cfg(feature = "v8")]
144#[cfg(not(feature = "qjs"))]
145mod v8;
146#[cfg(feature = "v8")]
147#[cfg(not(feature = "qjs"))]
148type Engine = v8::Engine;
149#[cfg(feature = "v8")]
150#[cfg(not(feature = "qjs"))]
151pub use v8::Error as JSError;
152
153#[cfg(feature = "qjs")]
154mod qjs;
155#[cfg(feature = "qjs")]
156type Engine = qjs::Engine;
157#[cfg(feature = "qjs")]
158pub use qjs::Error as JSError;
159
160#[cfg(not(any(feature = "v8", feature = "qjs")))]
161compile_error!("At least one of the features 'v8' or 'qjs' must be enabled");
162
163use once_cell::sync::OnceCell;
164use serde::{Deserialize, Serialize};
165use std::{
166 borrow::Cow,
167 collections::BTreeMap,
168 path::{Path, PathBuf},
169 sync::mpsc::{self, Receiver, Sender},
170 thread,
171};
172pub static KATEX_VERSION: &str = "0.16.21";
173static KATEX_CODE: &str = concat!(
174 include_str!("./katex.min.js"),
175 r#"function renderToStringAndMacros(input) {
176 try {
177 const html = katex.renderToString(
178 input.latex,
179 Object.assign({}, input.options, { macros: input.macros })
180 );
181 for (let key in input.macros) if (typeof input.macros[key] !== "string") {
182 input.macros[key] = input.macros[key].tokens.map(token => token.text).reverse().join("");
183 }
184 return JSON.stringify({ html: html, macros: input.macros });
185 } catch (e) {
186 if (e instanceof katex.ParseError) {
187 for (let key in input.macros) if (typeof input.macros[key] !== "string") {
188 input.macros[key] = input.macros[key].tokens.map(token => token.text).reverse().join("");
189 }
190 return JSON.stringify({ error: e.message, macros: input.macros });
191 } else {
192 throw e;
193 }
194 }
195 }"#
196);
197
198#[derive(Clone, Debug, Serialize)]
199struct Input {
200 pub latex: String,
201 pub options: Options,
202 pub macros: BTreeMap<String, String>,
203}
204
205#[derive(Clone, Debug, Serialize)]
206#[serde(rename_all = "camelCase")]
207pub struct Options {
208 pub display_mode: bool,
209 pub output: KatexOutput,
210 pub leqno: bool,
211 pub fleqn: bool,
212 pub throw_on_error: bool,
213 pub error_color: Cow<'static, str>,
214 pub min_rule_thickness: Option<f64>,
215 pub color_is_text_color: bool,
216 pub max_size: f64,
217 pub max_expand: i32,
218 pub strict: Option<bool>,
219 pub trust: bool,
220 pub global_group: bool,
221}
222impl Default for Options {
223 fn default() -> Self {
224 Options {
225 display_mode: false,
226 output: KatexOutput::HtmlAndMathml,
227 leqno: false,
228 fleqn: false,
229 throw_on_error: true,
230 error_color: "#cc0000".into(),
231 min_rule_thickness: None,
232 color_is_text_color: false,
233 max_size: std::f64::INFINITY,
234 max_expand: 1000,
235 strict: None,
236 trust: false,
237 global_group: false,
238 }
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub enum KatexOutput {
245 Html,
246 Mathml,
247 HtmlAndMathml,
248}
249
250#[derive(Debug, Serialize, Deserialize)]
251#[serde(untagged)]
252enum Output {
253 Success { html: String, macros: BTreeMap<String, String> },
254 Error { error: String, macros: BTreeMap<String, String> },
255}
256
257struct KatexWorker(Sender<(Input, Sender<Result<Output, Error>>)>);
258static KATEX_WORKER: OnceCell<KatexWorker> = OnceCell::new();
259
260#[derive(Debug, thiserror::Error)]
261pub enum Error {
262 #[error("JS Error: {0}")]
263 JSError(#[from] JSError),
264 #[error("Recv Error: {0}")]
265 RecvError(#[from] mpsc::RecvError),
266 #[error("Send Error")]
267 SendError,
268 #[error("KaTeX Error: math: {latex}, macros: {macros:?}, error: {message}")]
269 KaTeXError { message: String, latex: String, macros: BTreeMap<String, String> },
270}
271
272pub fn set_cache(path: impl AsRef<Path>) {
273 init_katex_worker(Some(path.as_ref().to_path_buf()));
274}
275
276pub(crate) trait Core: Sized {
277 type Error;
278 fn new() -> Result<Self, Self::Error>;
280 fn new_with_snapshot(path: &Path) -> Result<Self, Self::Error>;
282 fn exec(&mut self, input: Input) -> Result<Output, Self::Error>;
283}
284
285fn init_katex_worker(cache: Option<PathBuf>) {
286 if KATEX_WORKER.get().is_some() {
287 return;
288 }
289 let (tx, rx): (Sender<(Input, Sender<Result<Output, Error>>)>, Receiver<(Input, Sender<Result<Output, Error>>)>) = mpsc::channel();
290 thread::spawn(move || {
291 let mut runtime =
292 if let Some(cache) = cache { <Engine as Core>::new_with_snapshot(&cache).unwrap() } else { <Engine as Core>::new().unwrap() };
293 for (katex_input, sender) in rx {
294 let res = runtime.exec(katex_input).map_err(Error::from);
295 sender.send(res).unwrap();
296 }
297 });
298 KATEX_WORKER.set(KatexWorker(tx)).ok().unwrap();
299}
300
301pub fn render(latex: &str) -> Result<String, Error> {
302 render_with_opts(latex, &Default::default(), &mut BTreeMap::new())
303}
304
305pub fn render_with_opts(latex: &str, options: &Options, macros: &mut BTreeMap<String, String>) -> Result<String, Error> {
306 let (tx, rx) = mpsc::channel();
307 let Some(worker) = KATEX_WORKER.get() else {
308 init_katex_worker(None);
309 return render_with_opts(latex, options, macros);
310 };
311
312 worker
313 .0
314 .send((Input { latex: latex.to_string(), options: options.clone(), macros: macros.clone() }, tx))
315 .map_err(|_| Error::SendError)?;
316 match rx.recv()?? {
317 Output::Success { html, macros: macros_value } => {
318 *macros = macros_value;
319 Ok(html)
320 }
321 Output::Error { error, macros: macros_value } => {
322 Err(Error::KaTeXError { message: error, latex: latex.to_string(), macros: macros_value })
323 }
324 }
325}
326
327pub use font::{UsedFonts, font_extract};