math_core/lib.rs
1//! Convert LaTeX math to MathML Core.
2//!
3//! For more background on what that means and on what to do with the resulting MathML code,
4//! see the repo's README: https://github.com/tmke8/math-core
5//!
6//! # Usage
7//!
8//! The main struct of this library is [`LatexToMathML`]. In order to use the library, create an
9//! instance of this struct and then call one of the convert functions. The constructor of the
10//! struct expects a config object in the form of an instance of [`MathCoreConfig`].
11//!
12//! Basic use looks like this:
13//!
14//! ```rust
15//! use math_core::{LatexToMathML, MathCoreConfig, MathDisplay};
16//!
17//! let latex = r#"\erf ( x ) = \frac{ 2 }{ \sqrt{ \pi } } \int_0^x e^{- t^2} \, dt"#;
18//! let config = MathCoreConfig::default();
19//! let converter = LatexToMathML::new(config).unwrap();
20//! let result = converter.convert_with_local_state(latex, MathDisplay::Block).unwrap();
21//! println!("{}", result.mathml);
22//! ```
23//!
24//! # Features
25//!
26//! - `serde`: With this feature, `MathCoreConfig` implements serde's `Deserialize`.
27//!
28mod atof;
29mod character_class;
30mod color_defs;
31mod commands;
32mod environments;
33mod error;
34mod global_state;
35mod html_utils;
36mod lexer;
37mod parser;
38mod predefined;
39mod specifications;
40mod split_on_ascii;
41mod text_parser;
42mod token;
43mod token_queue;
44
45use rustc_hash::{FxBuildHasher, FxHashMap};
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48
49pub use mathml_renderer::ast::{CssClassNames, IndentKeyword, Indentation, Warnings};
50use mathml_renderer::{
51 arena::Arena,
52 ast::{Emitter, Node},
53 attribute::Style,
54 fmt::new_line_and_indent,
55};
56
57pub use self::error::LatexError;
58use self::{
59 error::LatexErrKind, global_state::GlobalState, lexer::Lexer, parser::Parser, token::Token,
60};
61
62/// Display mode for the LaTeX math equations.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum MathDisplay {
65 /// For inline equations, like those in `$...$` in LaTeX.
66 Inline,
67 /// For block equations (or "display style" equations), like those in `$$...$$` in LaTeX.
68 Block,
69}
70
71/// Configuration for pretty-printing the MathML output.
72///
73/// Pretty-printing means that newlines and indentation is added to the MathML output, to make it
74/// easier to read.
75#[derive(Debug, Clone, Copy, Default)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
78#[non_exhaustive]
79pub enum PrettyPrint {
80 /// Never pretty print.
81 #[default]
82 Never,
83 /// Always pretty print.
84 Always,
85 /// Pretty print for block equations only.
86 Auto,
87}
88
89/// Configuration for using Unicode symbols in the MathML output.
90///
91/// LaTeX commands like `\coloneqq` can be rendered in MathML either using dedicated Unicode symbols
92/// (in this case, `\coloneqq` would be rendered as `≔`) or using a combination of more basic
93/// symbols (in this case, `\coloneqq` would be rendered as a combination of `:` and `=`).
94/// The former is preferable in terms of semantics but can look a little different from the LaTeX
95/// output, while the latter is more faithful to the LaTeX output but can be less semantically
96/// clear.
97#[derive(Debug, Clone, Copy, Default)]
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
99#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
100#[non_exhaustive]
101pub enum UnicodeSubstitution {
102 /// Never subtitute a set of symbols with their Unicode equivalents.
103 Never,
104 /// Substitute whenever the LaTeX package `unicode-math` would substitute, which is a good
105 /// middle ground between semantics and faithfulness to the LaTeX output.
106 #[default]
107 Conventional,
108 // /// Substitute whenever there is a Unicode equivalent, even if the `unicode-math` package
109 // /// does not do so.
110 // Aggressive,
111}
112
113/// Configuration object for the LaTeX to MathML conversion.
114///
115/// # Example usage
116///
117/// ```rust
118/// use math_core::{MathCoreConfig, PrettyPrint};
119///
120/// // Default values
121/// let config = MathCoreConfig::default();
122///
123/// // Specifying pretty-print behavior
124/// let config = MathCoreConfig {
125/// pretty_print: PrettyPrint::Always,
126/// ..Default::default()
127/// };
128///
129/// // Specifying pretty-print behavior and custom macros
130/// let macros = vec![
131/// ("d".to_string(), r"\mathrm{d}".to_string()),
132/// ("bb".to_string(), r"\mathbb{#1}".to_string()), // with argument
133/// ];
134/// let config = MathCoreConfig {
135/// pretty_print: PrettyPrint::Auto,
136/// macros,
137/// ..Default::default()
138/// };
139/// ```
140///
141#[derive(Debug, Default)]
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
144pub struct MathCoreConfig {
145 /// A configuration for pretty-printing the MathML output. See [`PrettyPrint`] for details.
146 pub pretty_print: PrettyPrint,
147 /// A list of LaTeX macros; each tuple contains (macro_name, macro_definition).
148 #[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
149 pub macros: Vec<(String, String)>,
150 /// If `true`, include `xmlns="http://www.w3.org/1998/Math/MathML"` in the `<math>` tag.
151 pub xml_namespace: bool,
152 /// If `true`, unknown commands will be rendered as red text in the output, instead of
153 /// returning an error.
154 pub ignore_unknown_commands: bool,
155 /// If `true`, wrap the MathML output in `<semantics>` tags with an
156 /// `<annotation encoding="application/x-tex">` child containing the original LaTeX source.
157 pub annotation: bool,
158 /// If `true`, allow rendering commands that produce MathML Core output that is unreliably
159 /// rendered by browsers.
160 pub allow_unreliable_rendering: bool,
161 /// If not `UnicodeSubstitution::Never`, substitute certain LaTeX commands with their Unicode
162 /// equivalents in the MathML output.
163 pub unicode_substitution: UnicodeSubstitution,
164 /// CSS class names for various elements in the output.
165 pub css_classes: CssClassNames,
166 /// The indentation unit used when pretty-printing the MathML output. Either a number of spaces
167 /// (e.g. `2`) or the string `"tab"` for a tab character. See [`Indentation`].
168 pub indentation: Indentation,
169}
170
171/// A map from custom command names to their number of arguments and the slice of tokens that
172/// defines the command. The tokens are stored in a separate vector.
173type CustomCmdMap = FxHashMap<String, (u8, (usize, usize))>;
174
175/// Subset of `MathCoreConfig` relevant for the parser.
176#[derive(Debug, Default)]
177struct ParserConfig {
178 custom_cmd_tokens: Vec<Token<'static>>,
179 custom_cmd_map: CustomCmdMap,
180 ignore_unknown_commands: bool,
181 allow_unreliable_rendering: bool,
182 unicode_substitution: UnicodeSubstitution,
183}
184
185impl ParserConfig {
186 pub fn get_command<'config>(&'config self, command: &str) -> Option<Token<'config>> {
187 let (num_args, slice) = *self.custom_cmd_map.get(command)?;
188 let tokens = self.custom_cmd_tokens.get(slice.0..slice.1)?;
189 Some(Token::CustomCmd(num_args, tokens))
190 }
191}
192
193/// Subset of `MathCoreConfig` relevant for the emitter.
194#[derive(Debug, Default)]
195struct EmitterConfig {
196 pretty_print: PrettyPrint,
197 xml_namespace: bool,
198 annotation: bool,
199 css_classes: CssClassNames,
200 indentation: Indentation,
201}
202
203impl From<MathCoreConfig> for EmitterConfig {
204 fn from(config: MathCoreConfig) -> Self {
205 // FIXME: can we use a macro here to avoid repeating the field names?
206 Self {
207 pretty_print: config.pretty_print,
208 xml_namespace: config.xml_namespace,
209 annotation: config.annotation,
210 css_classes: config.css_classes,
211 indentation: config.indentation,
212 }
213 }
214}
215
216type ParseResult<T> = Result<T, Box<LatexError>>;
217
218/// The error type returned when parsing a custom macro definition fails. Contains the parsing
219/// error, the index of the macro definition in the `macros` vector and the macro definition itself.
220pub type MacroParseError = (Box<LatexError>, usize, String);
221
222/// A converter that transforms LaTeX math equations into MathML Core.
223#[derive(Debug, Default)]
224pub struct LatexToMathML {
225 emitter_cfg: EmitterConfig,
226 state: GlobalState,
227 parser_cfg: ParserConfig,
228}
229
230impl LatexToMathML {
231 /// Create a new `LatexToMathML` converter with the given configuration.
232 ///
233 /// This function returns an error if the custom macros in the given configuration could not
234 /// be parsed. The error contains the parsing error, the macro index and the macro definition
235 /// that caused the error.
236 pub fn new(mut config: MathCoreConfig) -> Result<Self, MacroParseError> {
237 let (custom_cmd_tokens, custom_cmd_map) = parse_custom_commands(
238 std::mem::take(&mut config.macros),
239 config.unicode_substitution,
240 )?;
241 let parser_cfg = ParserConfig {
242 custom_cmd_tokens,
243 custom_cmd_map,
244 ignore_unknown_commands: config.ignore_unknown_commands,
245 allow_unreliable_rendering: config.allow_unreliable_rendering,
246 unicode_substitution: config.unicode_substitution,
247 };
248 Ok(Self {
249 emitter_cfg: EmitterConfig::from(config),
250 state: GlobalState::default(),
251 parser_cfg,
252 })
253 }
254
255 /// Convert LaTeX to MathML with a global equation counter.
256 ///
257 /// For basic usage, see the documentation of [`convert_with_local_state`].
258 ///
259 /// This conversion function maintains state, in order to count equations correctly across
260 /// different calls to this function.
261 ///
262 /// The counter can be reset with [`reset_global_state`].
263 pub fn convert_with_global_state(
264 &mut self,
265 latex: &str,
266 display: MathDisplay,
267 ) -> Result<ConvertResult, Box<LatexError>> {
268 convert(
269 latex,
270 display,
271 &self.parser_cfg,
272 &mut self.state,
273 &self.emitter_cfg,
274 )
275 }
276
277 /// Convert LaTeX to MathML.
278 ///
279 /// The second argument specifies whether it is inline-equation or block-equation.
280 ///
281 /// ```rust
282 /// use math_core::{LatexToMathML, MathCoreConfig, MathDisplay};
283 ///
284 /// let latex = r#"(n + 1)! = \Gamma ( n + 1 )"#;
285 /// let config = MathCoreConfig::default();
286 /// let converter = LatexToMathML::new(config).unwrap();
287 /// let result = converter.convert_with_local_state(latex, MathDisplay::Inline).unwrap();
288 /// println!("{}", result.mathml);
289 ///
290 /// let latex = r#"x = \frac{ - b \pm \sqrt{ b^2 - 4 a c } }{ 2 a }"#;
291 /// let result = converter.convert_with_local_state(latex, MathDisplay::Block).unwrap();
292 /// println!("{}", result.mathml);
293 /// ```
294 ///
295 pub fn convert_with_local_state(
296 &self,
297 latex: &str,
298 display: MathDisplay,
299 ) -> Result<ConvertResult, Box<LatexError>> {
300 let mut state = GlobalState::default();
301 convert(
302 latex,
303 display,
304 &self.parser_cfg,
305 &mut state,
306 &self.emitter_cfg,
307 )
308 }
309
310 /// Reset the equation counter and the label map.
311 ///
312 /// This should normally be done at the beginning of a new document or section.
313 pub fn reset_global_state(&mut self) {
314 self.state.equation_count = 0;
315 self.state.label_map.clear();
316 }
317
318 /// Convert a collection of LaTeX snippets to MathML.
319 ///
320 /// This method handles *forward references* correctly, meaning that if an earlier snippet
321 /// contains a reference to an equation in a later snippet, the reference will be resolved
322 /// correctly. However, in order to achieve this, all snippets need to be parsed first and can
323 /// only then be emitted. This means you have to first extract all LaTeX snippets from your
324 /// document and then call this method with the whole set.
325 pub fn convert_all(
326 &self,
327 snippets: &[(&str, MathDisplay)],
328 ) -> Vec<Result<ConvertResult, Box<LatexError>>> {
329 let mut state = GlobalState::default();
330 let arena = Arena::new();
331 let ast_vec: Vec<ParseResult<(Vec<&Node<'_>>, &str, MathDisplay)>> = snippets
332 .iter()
333 .map(|(latex, display)| {
334 parse(latex, &arena, &self.parser_cfg, &mut state, *display)
335 .map(|ast| (ast, *latex, *display))
336 })
337 .collect::<Vec<_>>();
338 ast_vec
339 .into_iter()
340 .map(|ast_result| {
341 ast_result.map(|(ast, latex, display)| {
342 emit(
343 ast,
344 latex,
345 display,
346 &state.label_map,
347 &arena,
348 &self.emitter_cfg,
349 )
350 })
351 })
352 .collect()
353 }
354}
355
356fn convert(
357 latex: &str,
358 display: MathDisplay,
359 parser_cfg: &ParserConfig,
360 state: &mut GlobalState,
361 flags: &EmitterConfig,
362) -> Result<ConvertResult, Box<LatexError>> {
363 let arena = Arena::new();
364 let ast = parse(latex, &arena, parser_cfg, state, display)?;
365 Ok(emit(ast, latex, display, &state.label_map, &arena, flags))
366}
367
368fn emit(
369 ast: Vec<&Node>,
370 latex: &str,
371 display: MathDisplay,
372 label_map: &FxHashMap<Box<str>, Box<str>>,
373 arena: &Arena,
374 flags: &EmitterConfig,
375) -> ConvertResult {
376 let mut output = String::new();
377 output.push_str("<math");
378 if flags.xml_namespace {
379 output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
380 }
381 if matches!(display, MathDisplay::Block) {
382 output.push_str(" display=\"block\"");
383 }
384 output.push('>');
385
386 let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
387 || (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
388
389 let base_indent = if pretty_print { 1 } else { 0 };
390 let warnings: Warnings;
391 if flags.annotation {
392 let children_indent = if pretty_print { 2 } else { 0 };
393 new_line_and_indent(&mut output, base_indent, flags.indentation);
394 output.push_str("<semantics>");
395 let node = parser::node_vec_to_node(arena, &ast, false);
396 let mut emitter = Emitter::new(
397 std::mem::take(&mut output),
398 label_map,
399 &flags.css_classes,
400 flags.indentation,
401 );
402 let _ = emitter.emit(node, children_indent);
403 warnings = emitter.warnings();
404 output = emitter.into_string();
405 new_line_and_indent(&mut output, children_indent, flags.indentation);
406 output.push_str("<annotation encoding=\"application/x-tex\">");
407 html_utils::escape_html_content(&mut output, latex);
408 output.push_str("</annotation>");
409 new_line_and_indent(&mut output, base_indent, flags.indentation);
410 output.push_str("</semantics>");
411 } else {
412 let mut emitter = Emitter::new(
413 std::mem::take(&mut output),
414 label_map,
415 &flags.css_classes,
416 flags.indentation,
417 );
418 for node in ast {
419 // We ignore the result of `emit` here, because the only possible error is a formatting
420 // error when writing to the string, but `String`'s `write_str` implementation never
421 // returns an error.
422 let _ = emitter.emit(node, base_indent);
423 }
424 warnings = emitter.warnings();
425 output = emitter.into_string();
426 }
427 if pretty_print {
428 output.push('\n');
429 }
430 output.push_str("</math>");
431 ConvertResult {
432 mathml: output,
433 warnings,
434 }
435}
436
437/// The result of a LaTeX to MathML conversion.
438pub struct ConvertResult {
439 pub mathml: String,
440 pub warnings: Warnings,
441}
442
443fn parse<'arena>(
444 latex: &'arena str,
445 arena: &'arena Arena,
446 parser_cfg: &'arena ParserConfig,
447 state: &mut GlobalState,
448 display: MathDisplay,
449) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>> {
450 let style = match display {
451 MathDisplay::Inline => Style::Text,
452 MathDisplay::Block => Style::Display,
453 };
454 let lexer = Lexer::new(
455 latex,
456 false,
457 Some(parser_cfg),
458 parser_cfg.unicode_substitution,
459 );
460 let mut p = Parser::new(lexer, arena, state, style)?;
461 let nodes = p.parse()?;
462 Ok(nodes)
463}
464
465fn parse_custom_commands(
466 macros: Vec<(String, String)>,
467 unicode_substitution: UnicodeSubstitution,
468) -> Result<(Vec<Token<'static>>, CustomCmdMap), MacroParseError> {
469 let mut map = FxHashMap::with_capacity_and_hasher(macros.len(), FxBuildHasher);
470 let mut tokens = Vec::new();
471 for (idx, (name, definition)) in macros.into_iter().enumerate() {
472 if !is_valid_macro_name(name.as_str()) {
473 return Err((
474 Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
475 idx,
476 definition,
477 ));
478 }
479
480 // In order to be able to return `definition` in case of an error, we need to ensure
481 // that the lexer (which borrows `definition`) is dropped before we return the error.
482 // Therefore, we put the whole lexing process into its own block.
483 let value = 'value: {
484 let mut lexer: Lexer<'static, '_> =
485 Lexer::new(definition.as_str(), true, None, unicode_substitution);
486 let start = tokens.len();
487 loop {
488 match lexer.next_token_no_unknown_command() {
489 Ok(tokloc) => {
490 if matches!(tokloc.token(), Token::Eoi) {
491 break;
492 }
493 tokens.push(tokloc.into_token());
494 }
495 Err(err) => {
496 break 'value Err(err);
497 }
498 }
499 }
500 let end = tokens.len();
501 let num_args = lexer.parse_cmd_args().unwrap_or(0);
502 Ok((num_args, (start, end)))
503 };
504
505 match value {
506 Err(err) => {
507 return Err((err, idx, definition));
508 }
509 Ok(v) => {
510 map.insert(name, v);
511 }
512 }
513 }
514 Ok((tokens, map))
515}
516
517fn is_valid_macro_name(s: &str) -> bool {
518 if s.is_empty() {
519 return false;
520 }
521 let mut chars = s.chars();
522 match (chars.next(), chars.next()) {
523 // If the name contains only one character, any character is valid.
524 (Some(_), None) => true,
525 // If the name contains more than one character, all characters must be ASCII alphabetic.
526 _ => s.bytes().all(|b| b.is_ascii_alphabetic()),
527 }
528}