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}