at main 117 lines 3.7 kB view raw
1//! LaTeX math rendering via pulldown-latex → MathML 2 3use markdown_weaver_escape::escape_html; 4use pulldown_latex::{ 5 config::DisplayMode, config::RenderConfig, mathml::push_mathml, Parser, Storage, 6}; 7 8/// Result of attempting to render LaTeX math 9pub enum MathResult { 10 /// Successfully rendered MathML 11 Success(String), 12 /// Rendering failed - contains fallback HTML with source and error message 13 Error { html: String, message: String }, 14} 15 16/// Render LaTeX math to MathML 17/// 18/// # Arguments 19/// * `latex` - The LaTeX source string (without delimiters like $ or $$) 20/// * `display_mode` - If true, render as display math (block); if false, inline 21pub fn render_math(latex: &str, display_mode: bool) -> MathResult { 22 let storage = Storage::new(); 23 let parser = Parser::new(latex, &storage); 24 let config = RenderConfig { 25 display_mode: if display_mode { 26 DisplayMode::Block 27 } else { 28 DisplayMode::Inline 29 }, 30 ..Default::default() 31 }; 32 33 let mut mathml = String::new(); 34 35 // Collect events, tracking any errors 36 let events: Vec<_> = parser.collect(); 37 let errors: Vec<String> = events 38 .iter() 39 .filter_map(|e| e.as_ref().err().map(|err| err.to_string())) 40 .collect(); 41 42 if errors.is_empty() { 43 // All events parsed successfully - push_mathml wants the Results directly 44 if let Err(e) = push_mathml(&mut mathml, events.into_iter(), config) { 45 return MathResult::Error { 46 html: format_error_html(latex, &e.to_string(), display_mode), 47 message: e.to_string(), 48 }; 49 } 50 MathResult::Success(mathml) 51 } else { 52 // Had parse errors - return error HTML 53 let error_msg = errors.join("; "); 54 MathResult::Error { 55 html: format_error_html(latex, &error_msg, display_mode), 56 message: error_msg, 57 } 58 } 59} 60 61fn format_error_html(latex: &str, error: &str, display_mode: bool) -> String { 62 let mode_class = if display_mode { 63 "math-display" 64 } else { 65 "math-inline" 66 }; 67 let mut escaped_latex = String::new(); 68 let mut escaped_error = String::new(); 69 // These won't fail writing to String 70 let _ = escape_html(&mut escaped_latex, latex); 71 let _ = escape_html(&mut escaped_error, error); 72 format!( 73 r#"<span class="math math-error {mode_class}" title="{escaped_error}"><code>{escaped_latex}</code></span>"# 74 ) 75} 76 77#[cfg(test)] 78mod tests { 79 use super::*; 80 81 #[test] 82 fn renders_inline_math() { 83 let result = render_math("x^2", false); 84 assert!(matches!(result, MathResult::Success(_))); 85 if let MathResult::Success(mathml) = result { 86 assert!(mathml.contains("<math")); 87 assert!(mathml.contains("</math>")); 88 } 89 } 90 91 #[test] 92 fn renders_display_math() { 93 let result = render_math(r"\frac{a}{b}", true); 94 assert!(matches!(result, MathResult::Success(_))); 95 if let MathResult::Success(mathml) = result { 96 assert!(mathml.contains("<math")); 97 assert!(mathml.contains("<mfrac")); 98 } 99 } 100 101 #[test] 102 fn renders_complex_math() { 103 let result = render_math(r"\sum_{i=0}^{n} x_i", true); 104 assert!(matches!(result, MathResult::Success(_))); 105 } 106 107 #[test] 108 fn handles_invalid_latex() { 109 // Unclosed brace 110 let result = render_math(r"\frac{a", false); 111 assert!(matches!(result, MathResult::Error { .. })); 112 if let MathResult::Error { html, message } = result { 113 assert!(html.contains("math-error")); 114 assert!(!message.is_empty()); 115 } 116 } 117}