Skip to main content

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//! - `std` (enabled by default): Uses the Rust standard library. Disabling this feature (with
27//!   `default-features = false`) makes the crate `no_std`; the `alloc` crate is still required.
28//!   Note that disabling `std` also disables some speedups in dependencies (e.g. `memchr` then
29//!   can no longer use runtime CPU feature detection).
30//! - `serde`: With this feature, `MathCoreConfig` implements serde's `Serialize` and
31//!   `Deserialize`.
32//! - `ariadne`: Adds `LatexError::to_report()`, which converts an error into an
33//!   [`ariadne`](https://docs.rs/ariadne) report for pretty-printing the error together with a
34//!   source code snippet. The `ariadne` crate itself requires `std`, so this feature is not
35//!   usable on `no_std` targets.
36//!
37#![cfg_attr(not(any(feature = "std", test)), no_std)]
38
39extern crate alloc;
40
41mod atof;
42mod character_class;
43mod color_defs;
44mod commands;
45mod custom_cmds;
46mod environments;
47mod error;
48mod global_state;
49mod html_utils;
50mod lexer;
51mod parser;
52mod predefined;
53mod specifications;
54mod split_on_ascii;
55mod string_pool;
56mod text_parser;
57mod token;
58mod token_queue;
59
60use alloc::boxed::Box;
61use alloc::string::String;
62use alloc::vec::Vec;
63use core::ops::Range;
64
65use kstring::KString;
66use rustc_hash::FxBuildHasher;
67#[cfg(feature = "serde")]
68use serde::{Deserialize, Serialize};
69
70/// Hash map with a fast, non-cryptographic hasher, backed by `hashbrown` so it works in `no_std`.
71pub(crate) type FxHashMap<K, V> = hashbrown::HashMap<K, V, FxBuildHasher>;
72
73pub use mathml_renderer::ast::{CssClassNames, IndentKeyword, Indentation, Warnings};
74use mathml_renderer::{
75    arena::Arena,
76    ast::{Emitter, Node},
77    attribute::Style,
78    fmt::new_line_and_indent,
79};
80
81pub use self::error::LatexError;
82use self::{
83    commands::resolve_builtin_cmd,
84    custom_cmds::{CmdSource, CustomCmds, RecordedToken, is_valid_macro_name},
85    error::LatexErrKind,
86    global_state::GlobalState,
87    lexer::{Lexer, LexerOutput},
88    parser::Parser,
89    token::Token,
90};
91
92/// Display mode for the LaTeX math equations.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum MathDisplay {
95    /// For inline equations, like those in `$...$` in LaTeX.
96    Inline,
97    /// For block equations (or "display style" equations), like those in `$$...$$` in LaTeX.
98    Block,
99}
100
101/// Configuration for pretty-printing the MathML output.
102///
103/// Pretty-printing means that newlines and indentation is added to the MathML output, to make it
104/// easier to read.
105#[derive(Debug, Clone, Copy, Default)]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
108#[non_exhaustive]
109pub enum PrettyPrint {
110    /// Never pretty print.
111    #[default]
112    Never,
113    /// Always pretty print.
114    Always,
115    /// Pretty print for block equations only.
116    Auto,
117}
118
119/// Configuration for using Unicode symbols in the MathML output.
120///
121/// LaTeX commands like `\coloneqq` can be rendered in MathML either using dedicated Unicode symbols
122/// (in this case, `\coloneqq` would be rendered as `≔`) or using a combination of more basic
123/// symbols (in this case, `\coloneqq` would be rendered as a combination of `:` and `=`).
124/// The former is preferable in terms of semantics but can look a little different from the LaTeX
125/// output, while the latter is more faithful to the LaTeX output but can be less semantically
126/// clear.
127#[derive(Debug, Clone, Copy, Default)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
130#[non_exhaustive]
131pub enum UnicodeSubstitution {
132    /// Never subtitute a set of symbols with their Unicode equivalents.
133    Never,
134    /// Substitute whenever the LaTeX package `unicode-math` would substitute, which is a good
135    /// middle ground between semantics and faithfulness to the LaTeX output.
136    #[default]
137    Conventional,
138    // /// Substitute whenever there is a Unicode equivalent, even if the `unicode-math` package
139    // /// does not do so.
140    // Aggressive,
141}
142
143/// The maximum number of custom command expansions allowed in one snippet.
144///
145/// Names are resolved when a command is expanded, so a definition may refer to itself, directly or
146/// through other definitions, and expanding it would never end. Rather than detecting that, we
147/// simply stop after this many expansions, as LaTeX and KaTeX do.
148///
149/// The default is 1000, which is the same limit that KaTeX uses for its `maxExpand` setting.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
152pub struct MaxExpansions(pub u32);
153
154impl Default for MaxExpansions {
155    fn default() -> Self {
156        MaxExpansions(1000)
157    }
158}
159
160/// Configuration object for the LaTeX to MathML conversion.
161///
162/// # Example usage
163///
164/// ```rust
165/// use math_core::{MathCoreConfig, PrettyPrint};
166///
167/// // Default values
168/// let config = MathCoreConfig::default();
169///
170/// // Specifying pretty-print behavior
171/// let config = MathCoreConfig {
172///     pretty_print: PrettyPrint::Always,
173///     ..Default::default()
174///  };
175///
176/// // Specifying pretty-print behavior and custom macros
177/// let macros = vec![
178///     ("d".to_string(), r"\mathrm{d}".to_string()),
179///     ("bb".to_string(), r"\mathbb{#1}".to_string()), // with argument
180/// ];
181/// let config = MathCoreConfig {
182///     pretty_print: PrettyPrint::Auto,
183///     macros,
184///     ..Default::default()
185/// };
186/// ```
187///
188#[derive(Debug, Default)]
189#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
190#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
191pub struct MathCoreConfig {
192    /// A configuration for pretty-printing the MathML output. See [`PrettyPrint`] for details.
193    pub pretty_print: PrettyPrint,
194    /// A list of LaTeX macros; each tuple contains (macro_name, macro_definition).
195    ///
196    /// A macro may use another macro of this list, no matter which of the two comes first.
197    #[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
198    pub macros: Vec<(String, String)>,
199    /// If `true`, include `xmlns="http://www.w3.org/1998/Math/MathML"` in the `<math>` tag.
200    pub xml_namespace: bool,
201    /// If `true`, unknown commands will be rendered as red text in the output, instead of
202    /// returning an error.
203    pub ignore_unknown_commands: bool,
204    /// If `true`, wrap the MathML output in `<semantics>` tags with an
205    /// `<annotation encoding="application/x-tex">` child containing the original LaTeX source.
206    pub annotation: bool,
207    /// If `true`, allow rendering commands that produce MathML Core output that is unreliably
208    /// rendered by browsers.
209    pub allow_unreliable_rendering: bool,
210    /// If `true`, run the conversion in the global group, which means that commands defined at
211    /// the top level of a snippet with `\newcommand` (and related commands) stay defined for the
212    /// snippets which come after it.
213    ///
214    /// If `false` (the default), such definitions are local to the snippet which contains them.
215    /// This matches LaTeX, where constructs like `\begin{equation}` and `$$` open a local group,
216    /// and it matches the default behavior of KaTeX.
217    pub global_group: bool,
218    /// If not `UnicodeSubstitution::Never`, substitute certain LaTeX commands with their Unicode
219    /// equivalents in the MathML output.
220    pub unicode_substitution: UnicodeSubstitution,
221    /// CSS class names for various elements in the output.
222    pub css_classes: CssClassNames,
223    /// The indentation unit used when pretty-printing the MathML output. Either a number of spaces
224    /// (e.g. `2`) or the string `"tab"` for a tab character. See [`Indentation`].
225    pub indentation: Indentation,
226    /// How many custom commands may be expanded in one snippet before the conversion gives up.
227    /// See [`MaxExpansions`].
228    pub max_expansions: MaxExpansions,
229    /// Add this string to the start of every generated `id` attribute.
230    /// This is *not* URL escaped. Use the
231    /// [percent-encoding](https://crates.io/crates/percent-encoding) crate if you need to.
232    pub id_prefix: String,
233}
234
235/// Subset of `MathCoreConfig` relevant for the parser.
236#[derive(Debug, Default)]
237struct ParserConfig {
238    custom_cmds_from_cfg: CustomCmds,
239    ignore_unknown_commands: bool,
240    allow_unreliable_rendering: bool,
241    global_group: bool,
242    unicode_substitution: UnicodeSubstitution,
243    max_expansions: MaxExpansions,
244}
245
246/// Subset of `MathCoreConfig` relevant for the emitter.
247#[derive(Debug, Default)]
248struct EmitterConfig {
249    pretty_print: PrettyPrint,
250    xml_namespace: bool,
251    annotation: bool,
252    css_classes: CssClassNames,
253    indentation: Indentation,
254    id_prefix: String,
255}
256
257impl From<MathCoreConfig> for EmitterConfig {
258    fn from(config: MathCoreConfig) -> Self {
259        // FIXME: can we use a macro here to avoid repeating the field names?
260        Self {
261            pretty_print: config.pretty_print,
262            xml_namespace: config.xml_namespace,
263            annotation: config.annotation,
264            css_classes: config.css_classes,
265            indentation: config.indentation,
266            id_prefix: config.id_prefix,
267        }
268    }
269}
270
271type ParseResult<T> = Result<T, Box<LatexError>>;
272
273/// The error type returned when parsing a custom macro definition fails. Contains the parsing
274/// error, the index of the macro definition in the `macros` vector and the macro definition itself.
275pub type MacroParseError = (Box<LatexError>, usize, String);
276
277/// A converter that transforms LaTeX math equations into MathML Core.
278#[derive(Debug, Default)]
279pub struct LatexToMathML {
280    emitter_cfg: EmitterConfig,
281    state: GlobalState,
282    parser_cfg: ParserConfig,
283}
284
285impl LatexToMathML {
286    /// Create a new `LatexToMathML` converter with the given configuration.
287    ///
288    /// This function returns an error if the custom macros in the given configuration could not
289    /// be parsed. The error contains the parsing error, the macro index and the macro definition
290    /// that caused the error.
291    pub fn new(mut config: MathCoreConfig) -> Result<Self, MacroParseError> {
292        let custom_cmds = parse_custom_commands(
293            core::mem::take(&mut config.macros),
294            config.unicode_substitution,
295            config.allow_unreliable_rendering,
296        )?;
297        let parser_cfg = ParserConfig {
298            custom_cmds_from_cfg: custom_cmds,
299            ignore_unknown_commands: config.ignore_unknown_commands,
300            allow_unreliable_rendering: config.allow_unreliable_rendering,
301            global_group: config.global_group,
302            unicode_substitution: config.unicode_substitution,
303            max_expansions: config.max_expansions,
304        };
305        Ok(Self {
306            emitter_cfg: EmitterConfig::from(config),
307            state: GlobalState::default(),
308            parser_cfg,
309        })
310    }
311
312    /// Convert LaTeX to MathML with a global equation counter.
313    ///
314    /// For basic usage, see the documentation of [`Self::convert_with_local_state`].
315    ///
316    /// This conversion function maintains state, in order to count equations correctly across
317    /// different calls to this function.
318    ///
319    /// The counter can be reset with [`Self::reset_global_state`].
320    pub fn convert_with_global_state(
321        &mut self,
322        latex: &str,
323        display: MathDisplay,
324    ) -> Result<ConvertResult, Box<LatexError>> {
325        convert(
326            latex,
327            display,
328            &self.parser_cfg,
329            &mut self.state,
330            &self.emitter_cfg,
331        )
332    }
333
334    /// Convert LaTeX to MathML.
335    ///
336    /// The second argument specifies whether it is inline-equation or block-equation.
337    ///
338    /// ```rust
339    /// use math_core::{LatexToMathML, MathCoreConfig, MathDisplay};
340    ///
341    /// let latex = r#"(n + 1)! = \Gamma ( n + 1 )"#;
342    /// let config = MathCoreConfig::default();
343    /// let converter = LatexToMathML::new(config).unwrap();
344    /// let result = converter.convert_with_local_state(latex, MathDisplay::Inline).unwrap();
345    /// println!("{}", result.mathml);
346    ///
347    /// let latex = r#"x = \frac{ - b \pm \sqrt{ b^2 - 4 a c } }{ 2 a }"#;
348    /// let result = converter.convert_with_local_state(latex, MathDisplay::Block).unwrap();
349    /// println!("{}", result.mathml);
350    /// ```
351    ///
352    pub fn convert_with_local_state(
353        &self,
354        latex: &str,
355        display: MathDisplay,
356    ) -> Result<ConvertResult, Box<LatexError>> {
357        let mut state = GlobalState::default();
358        convert(
359            latex,
360            display,
361            &self.parser_cfg,
362            &mut state,
363            &self.emitter_cfg,
364        )
365    }
366
367    /// Reset the equation counter, the label map and the commands defined with `\newcommand`.
368    ///
369    /// This should normally be done at the beginning of a new document or section.
370    pub fn reset_global_state(&mut self) {
371        self.state.equation_count = 0;
372        self.state.label_map.clear();
373        self.state.custom_cmds.clear();
374    }
375
376    /// Convert a collection of LaTeX snippets to MathML.
377    ///
378    /// This method handles *forward references* correctly, meaning that if an earlier snippet
379    /// contains a reference to an equation in a later snippet, the reference will be resolved
380    /// correctly. However, in order to achieve this, all snippets need to be parsed first and can
381    /// only then be emitted. This means you have to first extract all LaTeX snippets from your
382    /// document and then call this method with the whole set.
383    pub fn convert_all<S: AsRef<str>>(
384        &self,
385        snippets: &[(S, MathDisplay)],
386    ) -> Vec<Result<ConvertResult, Box<LatexError>>> {
387        let mut state = GlobalState::default();
388        let arena = Arena::new();
389        let ast_vec: Vec<ParseResult<(Vec<&Node<'_>>, &str, MathDisplay)>> = snippets
390            .iter()
391            .map(|(latex, display)| {
392                let latex = latex.as_ref();
393                parse(latex, &arena, &self.parser_cfg, &mut state, *display)
394                    .map(|ast| (ast, latex, *display))
395            })
396            .collect::<Vec<_>>();
397        ast_vec
398            .into_iter()
399            .map(|ast_result| {
400                ast_result.map(|(ast, latex, display)| {
401                    emit(
402                        ast,
403                        latex,
404                        display,
405                        &state.label_map,
406                        &arena,
407                        &self.emitter_cfg,
408                    )
409                })
410            })
411            .collect()
412    }
413}
414
415fn convert(
416    latex: &str,
417    display: MathDisplay,
418    parser_cfg: &ParserConfig,
419    state: &mut GlobalState,
420    flags: &EmitterConfig,
421) -> Result<ConvertResult, Box<LatexError>> {
422    let arena = Arena::new();
423    let ast = parse(latex, &arena, parser_cfg, state, display)?;
424    Ok(emit(ast, latex, display, &state.label_map, &arena, flags))
425}
426
427fn emit(
428    ast: Vec<&Node>,
429    latex: &str,
430    display: MathDisplay,
431    label_map: &FxHashMap<KString, KString>,
432    arena: &Arena,
433    flags: &EmitterConfig,
434) -> ConvertResult {
435    let mut output = String::new();
436    output.push_str("<math");
437    if flags.xml_namespace {
438        output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
439    }
440    if matches!(display, MathDisplay::Block) {
441        output.push_str(" display=\"block\"");
442    }
443    output.push('>');
444
445    let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
446        || (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
447
448    let base_indent = if pretty_print { 1 } else { 0 };
449    let warnings: Warnings;
450    if flags.annotation {
451        let children_indent = if pretty_print { 2 } else { 0 };
452        new_line_and_indent(&mut output, base_indent, flags.indentation);
453        output.push_str("<semantics>");
454        let node = parser::node_vec_to_node(arena, &ast, false);
455        let mut emitter = Emitter::new(
456            core::mem::take(&mut output),
457            label_map,
458            &flags.css_classes,
459            flags.indentation,
460            &flags.id_prefix,
461        );
462        let _ = emitter.emit(node, children_indent);
463        warnings = emitter.warnings();
464        output = emitter.into_string();
465        new_line_and_indent(&mut output, children_indent, flags.indentation);
466        output.push_str("<annotation encoding=\"application/x-tex\">");
467        html_utils::escape_html_content(&mut output, latex);
468        output.push_str("</annotation>");
469        new_line_and_indent(&mut output, base_indent, flags.indentation);
470        output.push_str("</semantics>");
471    } else {
472        let mut emitter = Emitter::new(
473            core::mem::take(&mut output),
474            label_map,
475            &flags.css_classes,
476            flags.indentation,
477            &flags.id_prefix,
478        );
479        for node in ast {
480            // We ignore the result of `emit` here, because the only possible error is a formatting
481            // error when writing to the string, but `String`'s `write_str` implementation never
482            // returns an error.
483            let _ = emitter.emit(node, base_indent);
484        }
485        warnings = emitter.warnings();
486        output = emitter.into_string();
487    }
488    if pretty_print {
489        output.push('\n');
490    }
491    output.push_str("</math>");
492    ConvertResult {
493        mathml: output,
494        warnings,
495    }
496}
497
498/// The result of a LaTeX to MathML conversion.
499pub struct ConvertResult {
500    pub mathml: String,
501    pub warnings: Warnings,
502}
503
504fn parse<'arena>(
505    latex: &'arena str,
506    arena: &'arena Arena,
507    parser_cfg: &'arena ParserConfig,
508    state: &mut GlobalState,
509    display: MathDisplay,
510) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>> {
511    let style = match display {
512        MathDisplay::Inline => Style::Text,
513        MathDisplay::Block => Style::Display,
514    };
515    let lexer = Lexer::new(latex);
516    let mut p = Parser::new(lexer, arena, parser_cfg, state, style)?;
517    let nodes = p.parse()?;
518    Ok(nodes)
519}
520
521/// Read the macros of the configuration into a store of custom commands.
522///
523/// As in a body recorded from a `\newcommand`, every command is kept as a
524/// [`RecordedToken::CommandName`] and only resolved when the macro is used. A macro may
525/// therefore refer to a command which is not defined here at all, and in particular to another
526/// macro of the configuration, no matter in which order the two are given. Once all macros have
527/// been read, every one of those references must point at something, which is what the final
528/// check is for; a document definition, which has no such point in time, is only checked when
529/// it is used.
530fn parse_custom_commands(
531    macros: Vec<(String, String)>,
532    unicode_substitution: UnicodeSubstitution,
533    allow_unreliable_rendering: bool,
534) -> Result<CustomCmds, MacroParseError> {
535    let mut custom_cmds = CustomCmds::with_capacity(macros.len());
536    // The names which have to be defined by the time all macros have been read, together with
537    // the macro they appear in and their position within its definition.
538    let mut unresolved: Vec<(usize, KString, Range<usize>)> = Vec::new();
539    let mut body = Vec::new();
540    let parser_cfg = ParserConfig {
541        unicode_substitution,
542        allow_unreliable_rendering,
543        ..Default::default()
544    };
545    // The definitions are kept around, because the check at the end has to be able to report
546    // the one which contains an unresolved name.
547    let mut definitions: Vec<String> = Vec::with_capacity(macros.len());
548    for (idx, (name, definition)) in macros.into_iter().enumerate() {
549        if !is_valid_macro_name(name.as_str()) {
550            return Err((
551                Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
552                idx,
553                definition,
554            ));
555        }
556
557        body.clear();
558        let mut num_args = 0;
559        let mut first_class: Option<character_class::Class> = None;
560        let result = 'body: {
561            let mut lexer = Lexer::new(definition.as_str());
562            loop {
563                match lexer.next_token() {
564                    Ok(lexer_output) => {
565                        let token = match lexer_output {
566                            LexerOutput::CommandName(cmd_name, span) => {
567                                // We resolve the command here only to know whether it *can* be
568                                // resolved and to know its class. The actual resolution is done
569                                // when the macro is used.
570                                if let Some(resolved) = resolve_builtin_cmd(&parser_cfg, cmd_name) {
571                                    if first_class.is_none() {
572                                        first_class = resolved.class();
573                                    }
574                                } else {
575                                    unresolved.push((
576                                        idx,
577                                        KString::from_ref(cmd_name),
578                                        span.into(),
579                                    ));
580                                }
581                                body.push(RecordedToken::CommandName(KString::from_ref(cmd_name)));
582                                continue;
583                            }
584                            LexerOutput::Token(tokspan) => tokspan.into_token(),
585                        };
586                        match token {
587                            Token::Eoi => break,
588                            Token::CustomCmdArgInput(n) => {
589                                if n >= num_args {
590                                    num_args = n + 1;
591                                }
592                                body.push(RecordedToken::Token(Token::CustomCmdArg(n)));
593                            }
594                            tok => {
595                                if first_class.is_none() {
596                                    first_class = tok.class();
597                                }
598                                body.push(RecordedToken::Token(tok))
599                            }
600                        }
601                    }
602                    Err(err) => {
603                        break 'body Err(err);
604                    }
605                }
606            }
607            Ok(())
608        };
609
610        if let Err(err) = result {
611            return Err((err, idx, definition));
612        }
613        custom_cmds.insert(name.as_str(), num_args, &body, first_class);
614        // The lexer, which borrows the definition, is gone by now.
615        definitions.push(definition);
616    }
617    // Now that all macros are known, every name which none of them defines is an error.
618    for (idx, name, span) in unresolved {
619        if custom_cmds.get(&name, CmdSource::Config).is_none() {
620            let err = Box::new(LatexError(span, LatexErrKind::UnknownCommand(name)));
621            return Err((err, idx, definitions.swap_remove(idx)));
622        }
623    }
624    Ok(custom_cmds)
625}