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