katex_v8/
lib.rs

1use std::sync::Once;
2use v8;
3
4// Error handling
5#[derive(Debug)]
6pub enum Error {
7    JsInitError(String),
8    JsExecError(String),
9    JsValueError(String),
10}
11
12#[derive(Default)]
13pub struct Opts {
14    display_mode: Option<bool>,
15}
16
17impl Opts {
18    pub fn new() -> Self {
19        Default::default()
20    }
21
22    pub fn display_mode(mut self, value: bool) -> Self {
23        self.display_mode = Some(value);
24        self
25    }
26}
27
28static INIT: Once = Once::new();
29
30fn initialize_v8() {
31    INIT.call_once(|| {
32        let platform = v8::new_default_platform(0, false).make_shared();
33        v8::V8::initialize_platform(platform);
34        v8::V8::initialize();
35    });
36}
37
38// Helper trait to create v8::String
39trait IntoV8String {
40    fn to_v8_string<'s>(&self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String>;
41}
42
43impl<T: AsRef<str>> IntoV8String for T {
44    fn to_v8_string<'s>(&self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> {
45        // We use unwrap here because this conversion should never fail
46        // If it does fail (e.g., due to OOM), it's a catastrophic error anyway
47        v8::String::new(scope, self.as_ref()).unwrap()
48    }
49}
50
51pub fn inject_katex<'a>(
52    context: v8::Local<'a, v8::Context>,
53    scope: &mut v8::ContextScope<'a, v8::HandleScope<'_>>,
54) -> Result<v8::Local<'a, v8::Object>, Error> {
55    // Note: I'd love to use the ESM, but then we'd have to setup module loading.
56    // This is simpler for the moment to just load as a global `katex`.
57    let katex_src = include_str!("../vendor/katex.min.js").to_v8_string(scope);
58
59    v8::Script::compile(scope, katex_src, None)
60        .and_then(|script| script.run(scope))
61        .ok_or_else(|| Error::JsInitError("Failed to compile KaTeX script".to_string()))?;
62
63    let global = context.global(scope);
64    let katex_string = "katex".to_v8_string(scope);
65
66    let katex = global
67        .get(scope, katex_string.into())
68        .ok_or_else(|| Error::JsValueError("Failed to get KaTeX object".to_string()))?;
69    let katex_obj = v8::Local::<v8::Object>::try_from(katex);
70
71    match katex_obj {
72        Ok(obj) => Ok(obj),
73        Err(e) => Err(Error::JsValueError(format!(
74            "Failed to acquire KaTeX object: {}",
75            e
76        ))),
77    }
78}
79
80pub fn render(input: &str, opts: &Opts) -> Result<String, Error> {
81    initialize_v8();
82
83    let isolate = &mut v8::Isolate::new(Default::default());
84    let handle_scope = &mut v8::HandleScope::new(isolate);
85    let context = v8::Context::new(handle_scope, Default::default());
86    let scope = &mut v8::ContextScope::new(handle_scope, context);
87
88    let katex = inject_katex(context, scope)?;
89
90    let input = v8::String::new(scope, input).unwrap();
91    let opts_obj = v8::Object::new(scope);
92    if let Some(display_mode) = opts.display_mode {
93        let key = "displayMode".to_v8_string(scope);
94        let value = v8::Boolean::new(scope, display_mode);
95        opts_obj.set(scope, key.into(), value.into()).unwrap();
96    }
97
98    let render_to_string = "renderToString".to_v8_string(scope);
99
100    let render_func =
101        v8::Local::<v8::Function>::try_from(katex.get(scope, render_to_string.into()).unwrap())
102            .unwrap();
103
104    let args = &[input.into(), opts_obj.into()];
105    let result = render_func.call(scope, katex.into(), args).unwrap();
106
107    let result = result.to_string(scope).unwrap();
108    Ok(result.to_rust_string_lossy(scope))
109}