1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum MathDisplay {
46 Inline,
48 Block,
50}
51
52#[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 #[default]
62 Never,
63 Always,
65 Auto,
67}
68
69#[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 pub pretty_print: PrettyPrint,
109 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#[derive(Debug)]
137pub struct LatexToMathML {
138 pretty_print: PrettyPrint,
139 equation_count: usize,
141 custom_cmds: Option<CustomCmds>,
142}
143
144impl LatexToMathML {
145 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 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 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 #[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 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, 'config: 'source, {
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 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 (Some(_), None) => true,
323 _ => 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}