katex_gdef_v8/
lib.rs

1/*!
2# katex-gdef-v8
3
4A Rust library that utilizes KaTeX (v0.16.21) through the V8 engine to render LaTeX math expressions to HTML.
5
6## Features
7
8* **Fast Processing**: Rapid initialization and rendering using V8 snapshots
9* **Single Instance**: Reuse of a single KaTeX instance to minimize loading delays (though not optimized for parallel processing)
10* **Macro Support**: Collect and reuse macros defined with `\gdef` and similar commands (Note: depends on KaTeX v0.16.21 internal representation)
11* **Caching Capability**: Cache V8 snapshots to the filesystem to reduce startup time
12* **Font Detection**: Analyze rendered HTML to detect which KaTeX fonts are used
13
14## Installation
15
16Add this to your Cargo.toml:
17
18```toml
19[dependencies]
20katex-gdef-v8 = "0.1.6"
21```
22
23## Usage
24
25### Basic Example
26
27```rust
28use katex_gdef_v8::render;
29
30// KaTeX is initialized automatically on first call
31let html = render(r"E = mc^2").unwrap();
32println!("{}", html);
33```
34
35### Using Options and Macros
36
37```rust
38use katex_gdef_v8::{render_with_opts, Options, KatexOutput};
39use std::collections::BTreeMap;
40use std::borrow::Cow;
41
42let mut macros = BTreeMap::new();
43
44// Set custom options
45let options = Options {
46    display_mode: true,
47    output: KatexOutput::HtmlAndMathml,
48    error_color: Cow::Borrowed("#ff0000"),
49    ..Default::default()
50};
51
52// Render first equation (defining macros)
53let html1 = render_with_opts(
54    r"\gdef\myvar{x} \myvar^2 + \myvar = 0",
55    &options,
56    &mut macros
57).unwrap();
58println!("HTML 1: {}", html1);
59
60// Use previously defined macros in second equation
61let html2 = render_with_opts(
62    r"\myvar^3",
63    &options,
64    &mut macros
65).unwrap();
66println!("HTML 2: {}", html2);
67```
68
69### Font Detection
70
71The library can analyze rendered KaTeX HTML to determine which fonts are used:
72
73```rust
74use katex_gdef_v8::{render, font_extract};
75use std::collections::HashSet;
76
77// Render a LaTeX expression
78let html = render(r"\mathcal{F}(x) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} dx").unwrap();
79
80// Extract font information
81let used_fonts = font_extract(&html);
82
83// Check if specific fonts are used
84println!("Is empty: {}", used_fonts.is_empty());
85
86// Iterate through used fonts
87for font_name in used_fonts.clone() {
88    // Each font_name is the base name (e.g., "KaTeX_Math-Italic")
89    // To get the complete font file name, add file extension:
90    println!("Font file: {}.woff2", font_name);
91}
92
93// Collect all font names into a HashSet
94let font_set: HashSet<&str> = used_fonts.collect();
95
96// Example assertion for testing
97assert_eq!(
98    font_set,
99    HashSet::from([
100        "KaTeX_Main-Regular",
101        "KaTeX_Math-Italic",
102        "KaTeX_Size1-Regular",
103        "KaTeX_Caligraphic-Regular"
104    ])
105);
106```
107
108The `FontFlags` struct provides detailed information about all KaTeX fonts used in the rendered output, which can be useful for:
109
110- Optimizing font loading by only including required fonts (each font name can be used with extensions like `.woff2`, `.woff`, `.ttf`)
111- Selective font preloading in web applications
112
113### Setting Up Cache
114
115```rust
116use katex_gdef_v8::{set_cache, render};
117use std::path::Path;
118
119// Set path to cache V8 snapshot
120set_cache(Path::new("./katex-cache"));
121
122// Subsequent renderings will be faster
123let html = render(r"E = mc^2").unwrap();
124println!("{}", html);
125```
126
127## Comparison with `katex-rs`
128
129* **Macro collection and reuse**: Ability to reuse macros defined in equations in subsequent renderings (main differentiating feature)
130* **Caching capability**: Fast initialization with V8 snapshots
131* **Single-thread optimization**: Shared KaTeX instance in one worker thread (though not suitable for parallel processing)
132* **Font analysis**: Ability to detect which KaTeX fonts are used in the rendered output
133
134Note that `katex-rs` supports more JavaScript engines (duktape, wasm-js, etc.), making it more versatile in that respect.
135
136## License
137
138This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
139*/
140
141mod 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    // スナップショットを採れなかったとき
279    fn new() -> Result<Self, Self::Error>;
280    // snapshotを取り出す/または作成してからランタイムを返す
281    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};