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 mathml = converter.convert_with_local_counter(latex, MathDisplay::Block).unwrap();
21//! println!("{}", mathml);
22//! ```
23//!
24//! # Features
25//!
26//! - `serde`: With this feature, `MathCoreConfig` implements serde's `Deserialize`.
27//!
28mod latex_parser;
29mod mathml_renderer;
30mod raw_node_slice;
31
32use rustc_hash::FxHashMap;
33#[cfg(feature = "serde")]
34use serde::Deserialize;
35
36use self::latex_parser::{LatexErrKind, NodeRef, Token, node_vec_to_node};
37use self::mathml_renderer::arena::{Arena, FrozenArena};
38use self::mathml_renderer::ast::{MathMLEmitter, Node};
39use self::raw_node_slice::RawNodeSlice;
40
41pub use self::latex_parser::LatexError;
42
43/// Display mode for the LaTeX math equations.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum MathDisplay {
46    /// For inline equations, like those in `$...$` in LaTeX.
47    Inline,
48    /// For block equations (or "display style" equations), like those in `$$...$$` in LaTeX.
49    Block,
50}
51
52/// Configuration for pretty-printing the MathML output.
53///
54/// Pretty-printing means that newlines and indentation is added to the MathML output, to make it
55/// easier to read.
56#[derive(Debug, Clone, Copy, Default)]
57#[cfg_attr(feature = "serde", derive(Deserialize))]
58#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
59pub enum PrettyPrint {
60    /// Never pretty print.
61    #[default]
62    Never,
63    /// Always pretty print.
64    Always,
65    /// Pretty print for block equations only.
66    Auto,
67}
68
69/// Configuration object for the LaTeX to MathML conversion.
70///
71/// # Example usage
72///
73/// ```rust
74/// use math_core::{MathCoreConfig, PrettyPrint};
75/// use rustc_hash::FxHashMap;
76///
77/// // Default values
78/// let config = MathCoreConfig::default();
79///
80/// // Specifying pretty-print behavior
81/// let config = MathCoreConfig {
82///     pretty_print: PrettyPrint::Always,
83///     ..Default::default()
84///  };
85///
86/// // Specifying pretty-print behavior and custom macros
87/// let mut macros: FxHashMap<String, String> = Default::default();
88/// macros.insert(
89///     "d".to_string(),
90///     r"\mathrm{d}".to_string(),
91/// );
92/// macros.insert(
93///     "bb".to_string(),
94///     r"\mathbb{#1}".to_string(), // with argument
95/// );
96/// let config = MathCoreConfig {
97///     pretty_print: PrettyPrint::Auto,
98///     macros,
99///     ..Default::default()
100/// };
101/// ```
102///
103#[derive(Debug, Default)]
104#[cfg_attr(feature = "serde", derive(Deserialize))]
105#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
106pub struct MathCoreConfig {
107    /// A configuration for pretty-printing the MathML output. See [`PrettyPrint`] for details.
108    pub pretty_print: PrettyPrint,
109    /// A map of LaTeX macros; the keys are macro names and the values are their definitions.
110    pub macros: FxHashMap<String, String>,
111}
112
113struct CustomCmds {
114    arena: FrozenArena,
115    slice: RawNodeSlice,
116    map: FxHashMap<String, (usize, usize)>,
117}
118
119impl CustomCmds {
120    pub fn get_command<'config, 'source>(
121        &'config self,
122        command: &'source str,
123    ) -> Option<Token<'source>>
124    where
125        'config: 'source,
126    {
127        let (index, num_args) = *self.map.get(command)?;
128        let nodes = self.slice.lift(&self.arena)?;
129        let node = *nodes.get(index)?;
130        Some(Token::CustomCmd(num_args, NodeRef::new(node)))
131    }
132}
133
134/// A converter that transforms LaTeX math equations into MathML Core.
135pub struct LatexToMathML {
136    pretty_print: PrettyPrint,
137    /// This is used for numbering equations in the document.
138    equation_count: usize,
139    custom_cmds: Option<CustomCmds>,
140}
141
142impl LatexToMathML {
143    /// Create a new `LatexToMathML` converter with the given configuration.
144    /// 
145    /// This function returns an error if the custom macros in the given configuration could not
146    /// be parsed.
147    pub fn new(config: &MathCoreConfig) -> Result<Self, LatexError<'_>> {
148        Ok(Self {
149            pretty_print: config.pretty_print,
150            equation_count: 0,
151            custom_cmds: Some(parse_custom_commands(&config.macros)?),
152        })
153    }
154
155    /// Create a new `LatexToMathML` converter with default settings.
156    pub const fn const_default() -> Self {
157        Self {
158            pretty_print: PrettyPrint::Never,
159            equation_count: 0,
160            custom_cmds: None,
161        }
162    }
163
164    /// Convert LaTeX text to MathML with a global equation counter.
165    ///
166    /// For basic usage, see the documentation of [`convert_with_local_counter`].
167    ///
168    /// This conversion function maintains state, in order to count equations correctly across
169    /// different calls to this function.
170    ///
171    /// The counter can be reset with [`reset_global_counter`].
172    pub fn convert_with_global_counter<'config, 'source>(
173        &'config mut self,
174        latex: &'source str,
175        display: MathDisplay,
176    ) -> Result<String, LatexError<'source>>
177    where
178        'config: 'source,
179    {
180        convert(
181            latex,
182            display,
183            self.custom_cmds.as_ref(),
184            &mut self.equation_count,
185            self.pretty_print,
186        )
187    }
188
189    /// Convert LaTeX text to MathML.
190    ///
191    /// The second argument specifies whether it is inline-equation or block-equation.
192    ///
193    /// ```rust
194    /// use math_core::{LatexToMathML, MathCoreConfig, MathDisplay};
195    ///
196    /// let latex = r#"(n + 1)! = \Gamma ( n + 1 )"#;
197    /// let config = MathCoreConfig::default();
198    /// let converter = LatexToMathML::new(&config).unwrap();
199    /// let mathml = converter.convert_with_local_counter(latex, MathDisplay::Inline).unwrap();
200    /// println!("{}", mathml);
201    ///
202    /// let latex = r#"x = \frac{ - b \pm \sqrt{ b^2 - 4 a c } }{ 2 a }"#;
203    /// let mathml = converter.convert_with_local_counter(latex, MathDisplay::Block).unwrap();
204    /// println!("{}", mathml);
205    /// ```
206    ///
207    #[inline]
208    pub fn convert_with_local_counter<'config, 'source>(
209        &'config self,
210        latex: &'source str,
211        display: MathDisplay,
212    ) -> Result<String, LatexError<'source>>
213    where
214        'config: 'source,
215    {
216        let mut equation_count = 0;
217        convert(
218            latex,
219            display,
220            self.custom_cmds.as_ref(),
221            &mut equation_count,
222            self.pretty_print,
223        )
224    }
225
226    /// Reset the equation counter to zero.
227    ///
228    /// This should normally be done at the beginning of a new document or section.
229    pub fn reset_global_counter(&mut self) {
230        self.equation_count = 0;
231    }
232}
233
234fn convert<'config, 'source>(
235    latex: &'source str,
236    display: MathDisplay,
237    custom_cmds: Option<&'config CustomCmds>,
238    equation_count: &mut usize,
239    pretty_print: PrettyPrint,
240) -> Result<String, LatexError<'source>>
241where
242    'config: 'source,
243{
244    let arena = Arena::new();
245    let ast = parse(latex, &arena, custom_cmds)?;
246
247    let mut output = MathMLEmitter::new(equation_count);
248    match display {
249        MathDisplay::Block => output.push_str("<math display=\"block\">"),
250        MathDisplay::Inline => output.push_str("<math>"),
251    };
252
253    let pretty_print = matches!(pretty_print, PrettyPrint::Always)
254        || (matches!(pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
255
256    let base_indent = if pretty_print { 1 } else { 0 };
257    for node in ast {
258        output
259            .emit(node, base_indent)
260            .map_err(|_| LatexError(0, LatexErrKind::RenderError))?;
261    }
262    if pretty_print {
263        output.push('\n');
264    }
265    output.push_str("</math>");
266    Ok(output.into_inner())
267}
268
269fn parse<'config, 'arena, 'source>(
270    latex: &'source str,
271    arena: &'arena Arena,
272    custom_cmds: Option<&'config CustomCmds>,
273) -> Result<Vec<&'arena mathml_renderer::ast::Node<'arena>>, LatexError<'source>>
274where
275    'source: 'arena,  // 'source outlives 'arena
276    'config: 'source, // 'config outlives 'source
277{
278    let lexer = latex_parser::Lexer::new(latex, false, custom_cmds);
279    let mut p = latex_parser::Parser::new(lexer, arena);
280    let nodes = p.parse()?;
281    Ok(nodes)
282}
283
284fn parse_custom_commands<'source>(
285    macros: &'source FxHashMap<String, String>,
286) -> Result<CustomCmds, LatexError<'source>> {
287    let arena = Arena::new();
288    let mut map = FxHashMap::with_capacity_and_hasher(macros.len(), Default::default());
289    let mut parsed_macros = Vec::with_capacity(macros.len());
290    for (name, definition) in macros.iter() {
291        if !is_valid_macro_name(name) {
292            return Err(LatexError(0, LatexErrKind::InvalidMacroName(&name)));
293        }
294        let lexer = latex_parser::Lexer::new(definition, true, None);
295        let mut p = latex_parser::Parser::new(lexer, &arena);
296        let nodes = p.parse()?;
297        let num_args = p.l.parse_cmd_args.unwrap_or(0);
298
299        let node_ref = node_vec_to_node(&arena, nodes);
300        let index = parsed_macros.len();
301        parsed_macros.push(node_ref);
302        // TODO: avoid cloning `name` here
303        map.insert(name.clone(), (index, num_args));
304    }
305    let slice = RawNodeSlice::from_slice(arena.push_slice(&parsed_macros));
306    Ok(CustomCmds {
307        arena: arena.freeze(),
308        slice,
309        map,
310    })
311}
312
313fn is_valid_macro_name(s: &str) -> bool {
314    if s.is_empty() {
315        return false;
316    }
317    let mut chars = s.chars();
318    match (chars.next(), chars.next()) {
319        // If the name contains only one character, any character is valid.
320        (Some(_), None) => true,
321        // If the name contains more than one character, all characters must be ASCII alphabetic.
322        _ => s.bytes().all(|b| b.is_ascii_alphabetic()),
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use insta::assert_snapshot;
329
330    use crate::mathml_renderer::ast::MathMLEmitter;
331    use crate::{LatexErrKind, LatexError, LatexToMathML};
332
333    use super::{Arena, parse};
334
335    fn convert_content(latex: &str) -> Result<String, LatexError> {
336        let arena = Arena::new();
337        let nodes = parse(latex, &arena, None)?;
338        let mut equation_count = 0;
339        let mut emitter = MathMLEmitter::new(&mut equation_count);
340        for node in nodes.iter() {
341            emitter
342                .emit(node, 0)
343                .map_err(|_| LatexError(0, LatexErrKind::RenderError))?;
344        }
345        Ok(emitter.into_inner())
346    }
347
348    #[test]
349    fn full_tests() {
350        let problems = [
351            ("empty", r""),
352            ("only_whitespace", r"  "),
353            ("starts_with_whitespace", r"  x  "),
354            ("text", r"\text{hi}xx"),
355            ("text_multi_space", r"\text{x   y}"),
356            ("text_no_braces", r"\text x"),
357            ("text_no_braces_space_after", r"\text x y"),
358            ("text_no_braces_more_space", r"\text    xx"),
359            ("text_then_space", r"\text{x}~y"),
360            ("text_nested", r"\text{ \text{a}}"),
361            ("text_rq", r"\text{\rq}"),
362            (
363                "text_diacritics",
364                r#"\text{\'{a} \~{a} \.{a} \H{a} \`{a} \={a} \"{a} \v{a} \^{a} \u{a} \r{a} \c{c}}"#,
365            ),
366            ("text_with_escape_brace", r"\text{a\}b}"),
367            ("text_with_weird_o", r"\text{x\o y}"),
368            ("text_with_group", r"\text{x{y}z{}p{}}"),
369            ("text_with_special_symbols", r"\text{':,=-}"),
370            ("textbackslash", r"\text{\textbackslash}"),
371            ("textit", r"\textit{x}"),
372            ("textbf", r"\textbf{x}"),
373            ("textbf_with_digit", r"\textbf{1234}"),
374            ("textbf_with_digit_dot", r"\textbf{1234.}"),
375            ("textbf_with_digit_decimal", r"\textbf{1234.5}"),
376            ("texttt", r"\texttt{x}"),
377            ("mathtt", r"\mathtt{x}"),
378            ("mathtt_with_digit", r"\mathtt2"),
379            ("mathbf_with_digit", r"\mathbf{1234}"),
380            ("mathbf_with_digit_dot", r"\mathbf{1234.}"),
381            ("mathbf_with_digit_decimal", r"\mathbf{1234.5}"),
382            ("integer", r"0"),
383            ("rational_number", r"3.14"),
384            ("long_number", r"3,453,435.3453"),
385            ("number_with_dot", r"4.x"),
386            ("long_sub_super", r"x_{92}^{31415}"),
387            ("single_variable", r"x"),
388            ("greek_letter", r"\alpha"),
389            ("greek_letters", r"\phi/\varphi"),
390            (
391                "greek_letter_tf",
392                r"\Gamma\varGamma\boldsymbol{\Gamma\varGamma}",
393            ),
394            ("greek_letter_boldsymbol", r"\boldsymbol{\alpha}"),
395            ("simple_expression", r"x = 3+\alpha"),
396            ("sine_function", r"\sin x"),
397            ("sine_function_parens", r"\sin(x)"),
398            ("sine_function_sqbrackets", r"\sin[x]"),
399            ("sine_function_brackets", r"\sin\{x\}"),
400            ("square_root", r"\sqrt 2"),
401            ("square_root_without_space", r"\sqrt12"),
402            ("square_root_with_space", r"\sqrt 12"),
403            ("complex_square_root", r"\sqrt{x+2}"),
404            ("cube_root", r"\sqrt[3]{x}"),
405            ("simple_fraction", r"\frac{1}{2}"),
406            ("fraction_without_space", r"\frac12"),
407            ("fraction_with_space", r"\frac 12"),
408            ("slightly_more_complex_fraction", r"\frac{12}{5}"),
409            ("superscript", r"x^2"),
410            ("sub_superscript", r"x^2_3"),
411            ("super_subscript", r"x_3^2"),
412            ("double_subscript", r"g_{\mu\nu}"),
413            ("simple_accent", r"\dot{x}"),
414            ("operator_name", r"\operatorname{sn} x"),
415            ("operator_name_with_spaces", r"\operatorname{ hel lo }"),
416            ("operator_name_with_single_char", r"\operatorname{a}"),
417            ("operator_name_with_space_cmd", r"\operatorname{arg\,max}"),
418            ("simple_binomial_coefficient", r"\binom12"),
419            ("stretchy_parentheses", r"\left( x \right)"),
420            ("stretchy_one-sided_parenthesis", r"\left( x \right."),
421            ("simple_integral", r"\int dx"),
422            ("contour_integral", r"\oint_C dz"),
423            ("simple_overset", r"\overset{n}{X}"),
424            ("integral_with_bounds", r"\int_0^1 dx"),
425            ("integral_with_lower_bound", r"\int_0 dx"),
426            ("integral_with_upper_bound", r"\int^1 dx"),
427            ("integral_with_reversed_bounds", r"\int^1_0 dx"),
428            ("integral_with_complex_bound", r"\int_{0+1}^\infty"),
429            ("integral_with_limits", r"\int\limits_0^1 dx"),
430            ("integral_with_lower_limit", r"\int\limits_0 dx"),
431            ("integral_with_upper_limit", r"\int\limits^1 dx"),
432            ("integral_with_reversed_limits", r"\int\limits^1_0 dx"),
433            ("integral_pointless_limits", r"\int\limits dx"),
434            ("max_with_limits", r"\max\limits_x"),
435            ("bold_font", r"\bm{x}"),
436            ("black_board_font", r"\mathbb{R}"),
437            ("sum_with_special_symbol", r"\sum_{i = 0}^∞ i"),
438            ("sum_with_limit", r"\sum\limits_{i=1}^N"),
439            ("sum_pointless_limits", r"\sum\limits n"),
440            ("product", r"\prod_n n"),
441            ("underscore", r"x\ y"),
442            ("stretchy_brace", r"\left\{ x  ( x + 2 ) \right\}"),
443            ("stretchy_bracket", r"\left[ x  ( x + 2 ) \right]"),
444            ("matrix", r"\begin{pmatrix} x \\ y \end{pmatrix}"),
445            (
446                "align",
447                r#"\begin{align} f ( x ) &= x^2 + 2 x + 1 \\ &= ( x + 1 )^2\end{align}"#,
448            ),
449            ("align_star", r#"\begin{align*}x&=1\\y=2\end{align*}"#),
450            (
451                "text_transforms",
452                r#"{fi}\ \mathit{fi}\ \mathrm{fi}\ \texttt{fi}"#,
453            ),
454            ("colon_fusion", r"a := 2 \land b :\equiv 3"),
455            (
456                "cases",
457                r"f(x):=\begin{cases}0 &\text{if } x\geq 0\\1 &\text{otherwise.}\end{cases}",
458            ),
459            ("mathstrut", r"\mathstrut"),
460            ("greater_than", r"x > y"),
461            ("text_transform_sup", r"\mathbb{N} \cup \mathbb{N}^+"),
462            ("overbrace", r"\overbrace{a+b+c}^{d}"),
463            ("underbrace", r"\underbrace{a+b+c}_{d}"),
464            ("prod", r"\prod_i \prod^n \prod^n_i \prod_i^n"),
465            (
466                "scriptstyle",
467                r"\sum_{\genfrac{}{}{0pt}{}{\scriptstyle 0 \le i \le m}{\scriptstyle 0 < j < n}} P(i, j)",
468            ),
469            ("genfrac", r"\genfrac(]{0pt}{2}{a+b}{c+d}"),
470            ("genfrac_1pt", r"\genfrac(]{1pt}{2}{a+b}{c+d}"),
471            (
472                "genfrac_1pt_with_space",
473                r"\genfrac(]{  1pt     }{2}{a+b}{c+d}",
474            ),
475            ("genfrac_0.4pt", r"\genfrac(]{0.4pt}{2}{a+b}{c+d}"),
476            ("genfrac_0.4ex", r"\genfrac(]{0.4ex}{2}{a+b}{c+d}"),
477            ("genfrac_4em", r"\genfrac(]{4em}{2}{a+b}{c+d}"),
478            ("not_subset", r"\not\subset"),
479            ("not_less_than", r"\not\lt"),
480            ("not_less_than_symbol", r"\not< x"),
481            ("mathrm_with_superscript", r"\mathrm{x}^2"),
482            ("mathrm_with_sin", r"\mathrm{x\sin}"),
483            ("mathrm_with_sin2", r"\mathrm{\sin x}"),
484            ("mathrm_no_brackets", r"\mathrm x"),
485            ("mathit_no_brackets", r"\mathit x"),
486            ("mathbb_no_brackets", r"\mathbb N"),
487            ("mathit_of_max", r"\mathit{ab \max \alpha\beta}"),
488            ("mathit_of_operatorname", r"\mathit{a\operatorname{bc}d}"),
489            ("nested_transform", r"\mathit{\mathbf{a}b}"),
490            ("mathrm_nested", r"\mathit{\mathrm{a}b}"),
491            ("mathrm_nested2", r"\mathrm{\mathit{a}b}"),
492            ("mathrm_nested3", r"\mathrm{ab\mathit{cd}ef}"),
493            ("mathrm_nested4", r"\mathit{\mathrm{a}}"),
494            ("mathrm_multiletter", r"\mathrm{abc}"),
495            (
496                "complicated_operatorname",
497                r"\operatorname {{\pi} o \Angstrom a}",
498            ),
499            ("operatorname_with_other_operator", r"x\operatorname{\max}"),
500            (
501                "continued_fraction",
502                r"a_0 + \cfrac{1}{a_1 + \cfrac{1}{a_2 + \cfrac{1}{a_3 + \cfrac{1}{a_4}}}}",
503            ),
504            ("standalone_underscore", "_2F_3"),
505            ("really_standalone_underscore", "_2"),
506            ("standalone_superscript", "^2F_3"),
507            ("really_standalone_superscript", "^2"),
508            ("prime", r"f'"),
509            ("double_prime", r"f''"),
510            ("triple_prime", r"f'''"),
511            ("quadruple_prime", r"f''''"),
512            ("quintuple_prime", r"f'''''"),
513            ("prime_alone", "'"),
514            ("prime_and_super", r"f'^2"),
515            ("sub_prime_super", r"f_3'^2"),
516            ("double_prime_and_super", r"f''^2"),
517            ("double_prime_and_super_sub", r"f''^2_3"),
518            ("double_prime_and_sub_super", r"f''_3^2"),
519            ("sum_prime", r"\sum'"),
520            ("int_prime", r"\int'"),
521            ("vec_prime", r"\vec{x}'"),
522            ("overset_with_prime", r"\overset{!}{=}'"),
523            ("overset_prime", r"\overset{'}{=}"),
524            ("overset_plus", r"\overset{!}{+}"),
525            ("int_limit_prime", r"\int\limits'"),
526            ("prime_command", r"f^\prime"),
527            ("prime_command_braces", r"f^{\prime}"),
528            ("transform_group", r"\mathit{a{bc}d}"),
529            ("nabla_in_mathbf", r"\mathbf{\nabla} + \nabla"),
530            ("mathcal_vs_mathscr", r"\mathcal{A}, \mathscr{A}"),
531            ("vertical_line", r"P(x|y)"),
532            ("mid", r"P(x\mid y)"),
533            ("special_symbols", r"\%\$\#"),
534            ("lbrack_instead_of_bracket", r"\sqrt\lbrack 4]{2}"),
535            ("middle_vert", r"\left(\frac12\middle|\frac12\right)"),
536            (
537                "middle_uparrow",
538                r"\left(\frac12\middle\uparrow\frac12\right)",
539            ),
540            ("middle_bracket", r"\left(\frac12\middle]\frac12\right)"),
541            ("left_right_different_stretch", r"\left/\frac12\right)"),
542            ("RR_command", r"\RR"),
543            ("odv", r"\odv{f}{x}"),
544            ("xrightarrow", r"\xrightarrow{x}"),
545            ("slashed", r"\slashed{\partial}"),
546            ("plus_after_equal", r"x = +4"),
547            ("equal_after_plus", r"x+ = 4"),
548            ("plus_in_braces", r"4{+}4"),
549            ("equal_at_group_begin", r"x{=x}"),
550            ("plus_after_equal_subscript", r"x =_+4"),
551            ("plus_after_equal_subscript2", r"x =_2 +4"),
552            ("equal_equal", r"4==4"),
553            ("subscript_equal_equal", r"x_==4"),
554            ("color", r"{\color{Blue}x^2}"),
555            ("hspace", r"\hspace{1cm}"),
556            ("hspace_whitespace", r"\hspace{  4em }"),
557            ("hspace_whitespace_in_between", r"\hspace{  4  em }"),
558            ("array_simple", r"\begin{array}{lcr} 0 & 1 & 2 \end{array}"),
559            (
560                "array_lines",
561                r"\begin{array}{ |l| |rc| } 10 & 20 & 30\\ 4 & 5 & 6 \end{array}",
562            ),
563            (
564                "array_many_lines",
565                r"\begin{array}{ ||::|l } 10\\ 2 \end{array}",
566            ),
567            (
568                "subarray",
569                r"\sum_{\begin{subarray}{c} 0 \le i \le m\\ 0 < j < n \end{subarray}}",
570            ),
571        ];
572
573        let config = crate::MathCoreConfig {
574            pretty_print: crate::PrettyPrint::Always,
575            ..Default::default()
576        };
577        let converter = LatexToMathML::new(&config).unwrap();
578        for (name, problem) in problems.into_iter() {
579            let mathml = converter
580                .convert_with_local_counter(problem, crate::MathDisplay::Inline)
581                .expect(format!("failed to convert `{}`", problem).as_str());
582            assert_snapshot!(name, &mathml, problem);
583        }
584    }
585
586    #[test]
587    fn error_test() {
588        let problems = [
589            ("end_without_open", r"\end{matrix}"),
590            ("curly_close_without_open", r"}"),
591            ("unsupported_command", r"\asdf"),
592            (
593                "unsupported_environment",
594                r"\begin{xmatrix} 1 \end{xmatrix}",
595            ),
596            ("incorrect_bracket", r"\operatorname[lim}"),
597            ("unclosed_bracket", r"\sqrt[lim"),
598            ("mismatched_begin_end", r"\begin{matrix} 1 \end{bmatrix}"),
599            (
600                "spaces_in_env_name",
601                r"\begin{  pmatrix   } x \\ y \end{pmatrix}",
602            ),
603            (
604                "incorrect_bracket_in_begin",
605                r"\begin{matrix] 1 \end{matrix}",
606            ),
607            ("incomplete_sup", r"x^"),
608            ("invalid_sup", r"x^^"),
609            ("invalid_sub_sup", r"x^_"),
610            ("double_sub", r"x__3"),
611            ("int_double_sub", r"\int__3 x dx"),
612            ("unicode_command", r"\éx"),
613            ("wrong_opening_paren", r"\begin[matrix} x \end{matrix}"),
614            ("unclosed_brace", r"{"),
615            ("unclosed_left", r"\left( x"),
616            ("unclosed_env", r"\begin{matrix} x"),
617            ("unclosed_text", r"\text{hello"),
618            ("unexpected_limits", r"\text{hello}\limits_0^1"),
619            ("unsupported_not", r"\not\text{hello}"),
620            ("text_with_unclosed_group", r"\text{x{}"),
621            ("operatorname_with_end", r"\operatorname{\end{matrix}}"),
622            ("operatorname_with_begin", r"\operatorname{\begin{matrix}}"),
623            ("super_then_prime", "f^2'"),
624            ("sub_super_then_prime", "f_5^2'"),
625            ("sup_sup", "x^2^3 y"),
626            ("sub_sub", "x_2_3 y"),
627            ("no_rbrack_instead_of_bracket", r"\sqrt[3\rbrack{1}"),
628            ("genfrac_wrong_unit", r"\genfrac(]{1pg}{2}{a+b}{c+d}"),
629            ("hspace_empty", r"\hspace{  }"),
630            ("hspace_unknown_unit", r"\hspace{2ly}"),
631            ("hspace_non_digits", r"\hspace{2b2cm}"),
632            ("hspace_non_ascii", r"\hspace{22öm}"),
633        ];
634
635        for (name, problem) in problems.into_iter() {
636            let LatexError(loc, error) = convert_content(problem).unwrap_err();
637            let output = format!("Position: {}\n{:#?}", loc, error);
638            assert_snapshot!(name, &output, problem);
639        }
640    }
641
642    #[test]
643    fn test_custom_cmd_zero_arg() {
644        let macros = [
645            ("half".to_string(), r"\frac{1}{2}".to_string()),
646            ("mycmd".to_string(), r"\sqrt{3}".to_string()),
647        ]
648        .into_iter()
649        .collect();
650
651        let config = crate::MathCoreConfig {
652            macros,
653            pretty_print: crate::PrettyPrint::Always,
654        };
655
656        let converter = LatexToMathML::new(&config).unwrap();
657
658        let latex = r"x = \half";
659        let mathml = converter
660            .convert_with_local_counter(latex, crate::MathDisplay::Inline)
661            .unwrap();
662
663        assert_snapshot!("custom_cmd_zero_arg", mathml, latex);
664    }
665    #[test]
666    fn test_custom_cmd_one_arg() {
667        let macros = [
668            ("half".to_string(), r"\frac{1}{2}".to_string()),
669            ("mycmd".to_string(), r"\sqrt{#1}".to_string()),
670        ]
671        .into_iter()
672        .collect();
673
674        let config = crate::MathCoreConfig {
675            macros,
676            pretty_print: crate::PrettyPrint::Always,
677        };
678
679        let converter = LatexToMathML::new(&config).unwrap();
680
681        let latex = r"x = \mycmd{3}";
682        let mathml = converter
683            .convert_with_local_counter(latex, crate::MathDisplay::Inline)
684            .unwrap();
685
686        assert_snapshot!("custom_cmd_one_arg", mathml, latex);
687    }
688}