Skip to main content

latexml/
api.rs

1//! Stable, high-level conversion API for using `latexml` as a **library**.
2//!
3//! Downstream Rust crates depend on `latexml` and call one function — no
4//! binary, no manual [`Config`] or
5//! binding-dispatch wiring:
6//!
7//! ```no_run
8//! let xml = latexml::api::convert_to_xml(r"\documentclass{article}\begin{document}Hi\end{document}")?;
9//! let html = latexml::api::convert_to_html(r"\documentclass{article}\begin{document}Hi\end{document}")?;
10//! # Ok::<(), String>(())
11//! ```
12//!
13//! ## What these encapsulate
14//! The engine is a **thread-local singleton**, so each call runs on its own
15//! worker thread with a large (256 MiB) stack — matching the `latexml_oxide`
16//! binary — so deeply nested math can't overflow the 8 MiB default stack, and
17//! the thread's `#[thread_local]` engine roots (~110 MiB) are released via
18//! [`reset_thread_engine`](latexml_core::reset_thread_engine) before the thread
19//! exits (those roots do **not** run destructors on a bare thread exit). The
20//! standard package + contrib binding-dispatch chain is wired for you.
21//!
22//! ## Requirements at runtime
23//! Same host dependencies as the binary: a **TeX distribution** on `PATH` for
24//! packages/classes/fonts, and (only for figure-bearing HTML) the graphics
25//! tools. XML/XSLT/RelaxNG assets are embedded.
26//!
27//! For finer control (preloads, search paths, encoding, whatsin/out, split,
28//! …), drive [`crate::converter::Converter`] and [`crate::post`] directly; this
29//! module is the batteries-included entrypoint.
30
31use std::rc::Rc;
32
33use latexml_core::common::{Config, OutputFormat};
34
35use crate::{converter::Converter, post};
36
37/// Build the standard library `Config`: quiet, LaTeX mode, with the package +
38/// contrib binding-dispatch chain a normal conversion uses.
39fn library_config(format: OutputFormat) -> Config {
40  Config {
41    // Quiet by default: a library caller doesn't want progress notes on stderr.
42    // The per-conversion log is still captured internally and returned in the
43    // error path. Callers wanting logs can use `Converter` directly.
44    verbosity: -1,
45    format,
46    bindings_dispatch: Some(Rc::new(latexml_package::dispatch)),
47    extra_bindings_dispatch: Some(Rc::new(latexml_contrib::dispatch)),
48    ..Config::default()
49  }
50}
51
52/// Run `job` on a fresh 256 MiB-stack worker thread and free the thread-local
53/// engine before the thread exits. Mirrors `latexml_oxide::main`'s worker
54/// thread and `util::test`'s per-conversion `reset_thread_engine()`.
55fn on_worker<T: Send + 'static>(job: impl FnOnce() -> T + Send + 'static) -> T {
56  std::thread::Builder::new()
57    .stack_size(256 * 1024 * 1024)
58    .spawn(move || {
59      let out = job();
60      // `#[thread_local]` engine roots don't Drop on thread exit; free the
61      // ~110 MiB explicitly so repeated calls don't accumulate.
62      latexml_core::reset_thread_engine();
63      out
64    })
65    .expect("spawn latexml worker thread")
66    .join()
67    .expect("latexml worker thread panicked")
68}
69
70/// Convert a TeX/LaTeX source string to LaTeXML **XML** (the intermediate
71/// representation, before any HTML post-processing).
72///
73/// Returns the serialized XML on success, or the captured conversion log on
74/// failure (a fatal error, or an engine that could not initialize).
75pub fn convert_to_xml(tex: &str) -> Result<String, String> {
76  let tex = tex.to_string();
77  on_worker(move || {
78    let opts = library_config(OutputFormat::XML);
79    let mut converter = Converter::from_config(opts.clone());
80    converter
81      .prepare_session(&opts)
82      .map_err(|e| format!("could not prepare session: {e}"))?;
83    let response = converter.convert(format!("literal:{tex}"));
84    response
85      .result
86      .ok_or_else(|| format!("conversion failed:\n{}", response.log))
87  })
88}
89
90/// Convert a TeX/LaTeX source string all the way to a standalone **HTML5**
91/// document (LaTeXML XML + the HTML post-processing pipeline, with
92/// Presentation MathML).
93///
94/// Figures are left as raw `<ltx:graphics>` references — image conversion
95/// writes files next to a destination, and this string-returning API has none;
96/// use [`crate::post`] with a `destination` when you need converted images.
97pub fn convert_to_html(tex: &str) -> Result<String, String> {
98  let tex = tex.to_string();
99  on_worker(move || {
100    let opts = library_config(OutputFormat::HTML5);
101    let mut converter = Converter::from_config(opts.clone());
102    converter
103      .prepare_session(&opts)
104      .map_err(|e| format!("could not prepare session: {e}"))?;
105    let xml = converter
106      .convert(format!("literal:{tex}"))
107      .result
108      .ok_or_else(|| "conversion failed before post-processing".to_string())?;
109
110    let post_opts = post::PostOptions {
111      pmml:                      true,
112      cmml:                      false,
113      keep_xmath:                false,
114      // Same per-format sheet the CLI uses (shared source of truth).
115      stylesheet:                post::default_stylesheet(Some("html5")),
116      destination:               None,
117      source_directory:          None,
118      site_directory:            None,
119      search_paths:              &[],
120      nodefaultresources:        false,
121      css_files:                 &[],
122      js_files:                  &[],
123      noinvisibletimes:          false,
124      plane1:                    true,
125      hackplane1:                false,
126      mathtex:                   false,
127      navigationtoc:             None,
128      schemadocs:                false,
129      split:                     false,
130      split_xpath:               None,
131      split_naming:              None,
132      xslt_parameters:           &[],
133      graphics_svg_threshold_kb: 0,
134      // No destination to write images to; keep the raw graphics references.
135      graphicimages:             false,
136      timestamp:                 None,
137      icon:                      None,
138      whatsout:                  latexml_post::extract::Whatsout::Document,
139    };
140    Ok(post::run_post_processing(&xml, &post_opts))
141  })
142}
143
144#[cfg(test)]
145mod tests {
146  use super::*;
147
148  const DOC: &str = r"\documentclass{article}\begin{document}Hello \(x^2\)\end{document}";
149
150  #[test]
151  fn xml_conversion_returns_content() {
152    let xml = convert_to_xml(DOC).expect("convert_to_xml");
153    assert!(
154      xml.contains("Hello"),
155      "XML should contain the body text: {xml}"
156    );
157    assert!(
158      xml.contains("<?xml") || xml.contains("<document"),
159      "looks like XML"
160    );
161  }
162
163  #[test]
164  fn html_conversion_returns_page() {
165    let html = convert_to_html(DOC).expect("convert_to_html");
166    assert!(html.contains("Hello"), "HTML should contain the body text");
167    assert!(
168      html.contains("<math") || html.contains("ltx_Math"),
169      "math rendered"
170    );
171  }
172}