1mod atof;
29mod character_class;
30mod color_defs;
31mod commands;
32mod environments;
33mod error;
34mod html_utils;
35mod lexer;
36mod parser;
37mod predefined;
38mod specifications;
39mod text_parser;
40mod token;
41mod token_queue;
42
43use std::num::NonZeroU16;
44
45use rustc_hash::{FxBuildHasher, FxHashMap};
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48
49use mathml_renderer::{arena::Arena, ast::Node, attribute::Style, fmt::new_line_and_indent};
50
51pub use self::error::LatexError;
52use self::{error::LatexErrKind, lexer::Lexer, parser::Parser, token::Token};
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum MathDisplay {
57 Inline,
59 Block,
61}
62
63#[derive(Debug, Clone, Copy, Default)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
70#[non_exhaustive]
71pub enum PrettyPrint {
72 #[default]
74 Never,
75 Always,
77 Auto,
79}
80
81#[derive(Debug, Default)]
110#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
111#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
112pub struct MathCoreConfig {
113 pub pretty_print: PrettyPrint,
115 #[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
117 pub macros: Vec<(String, String)>,
118 pub xml_namespace: bool,
120 pub ignore_unknown_commands: bool,
123 pub annotation: bool,
126 pub allow_unreliable_rendering: bool,
129}
130
131#[derive(Debug, Default)]
132struct CommandConfig {
133 custom_cmd_tokens: Vec<Token<'static>>,
134 custom_cmd_map: FxHashMap<String, (u8, (usize, usize))>,
135 ignore_unknown_commands: bool,
136 allow_unreliable_rendering: bool,
137}
138
139impl CommandConfig {
140 pub fn get_command<'config>(&'config self, command: &str) -> Option<Token<'config>> {
141 let (num_args, slice) = *self.custom_cmd_map.get(command)?;
142 let tokens = self.custom_cmd_tokens.get(slice.0..slice.1)?;
143 Some(Token::CustomCmd(num_args, tokens))
144 }
145}
146
147#[derive(Debug, Default)]
149struct Flags {
150 pretty_print: PrettyPrint,
151 xml_namespace: bool,
152 annotation: bool,
153}
154
155impl From<&MathCoreConfig> for Flags {
156 fn from(config: &MathCoreConfig) -> Self {
157 Self {
159 pretty_print: config.pretty_print,
160 xml_namespace: config.xml_namespace,
161 annotation: config.annotation,
162 }
163 }
164}
165
166#[derive(Debug, Default)]
168pub struct LatexToMathML {
169 flags: Flags,
170 equation_count: u16,
172 label_map: FxHashMap<Box<str>, NonZeroU16>,
173 cmd_cfg: Option<CommandConfig>,
174}
175
176impl LatexToMathML {
177 pub fn new(config: MathCoreConfig) -> Result<Self, (Box<LatexError>, usize, String)> {
183 Ok(Self {
184 flags: Flags::from(&config),
185 equation_count: 0,
186 label_map: FxHashMap::default(),
187 cmd_cfg: Some(parse_custom_commands(config)?),
188 })
189 }
190
191 pub fn convert_with_global_counter(
200 &mut self,
201 latex: &str,
202 display: MathDisplay,
203 ) -> Result<String, Box<LatexError>> {
204 convert(
205 latex,
206 display,
207 self.cmd_cfg.as_ref(),
208 &mut self.equation_count,
209 &mut self.label_map,
210 &self.flags,
211 )
212 }
213
214 #[inline]
233 pub fn convert_with_local_counter(
234 &self,
235 latex: &str,
236 display: MathDisplay,
237 ) -> Result<String, Box<LatexError>> {
238 let mut equation_count = 0;
239 let mut label_map = FxHashMap::default();
240 convert(
241 latex,
242 display,
243 self.cmd_cfg.as_ref(),
244 &mut equation_count,
245 &mut label_map,
246 &self.flags,
247 )
248 }
249
250 pub fn reset_global_counter(&mut self) {
254 self.equation_count = 0;
255 }
256}
257
258fn convert(
259 latex: &str,
260 display: MathDisplay,
261 cmd_cfg: Option<&CommandConfig>,
262 equation_count: &mut u16,
263 label_map: &mut FxHashMap<Box<str>, NonZeroU16>,
264 flags: &Flags,
265) -> Result<String, Box<LatexError>> {
266 let arena = Arena::new();
267 let ast = parse(latex, &arena, cmd_cfg, equation_count, label_map, display)?;
268
269 let mut output = String::new();
270 output.push_str("<math");
271 if flags.xml_namespace {
272 output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
273 }
274 if matches!(display, MathDisplay::Block) {
275 output.push_str(" display=\"block\"");
276 }
277 output.push('>');
278
279 let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
280 || (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
281
282 let base_indent = if pretty_print { 1 } else { 0 };
283 if flags.annotation {
284 let children_indent = if pretty_print { 2 } else { 0 };
285 new_line_and_indent(&mut output, base_indent);
286 output.push_str("<semantics>");
287 let node = parser::node_vec_to_node(&arena, &ast, false);
288 let _ = node.emit(&mut output, children_indent);
289 new_line_and_indent(&mut output, children_indent);
290 output.push_str("<annotation encoding=\"application/x-tex\">");
291 html_utils::escape_html_content(&mut output, latex);
292 output.push_str("</annotation>");
293 new_line_and_indent(&mut output, base_indent);
294 output.push_str("</semantics>");
295 } else {
296 for node in ast {
297 let _ = node.emit(&mut output, base_indent);
302 }
303 }
304 if pretty_print {
305 output.push('\n');
306 }
307 output.push_str("</math>");
308 Ok(output)
309}
310
311fn parse<'config, 'source, 'arena>(
312 latex: &'source str,
313 arena: &'arena Arena,
314 cmd_cfg: Option<&'config CommandConfig>,
315 equation_count: &'arena mut u16,
316 label_map: &'arena mut FxHashMap<Box<str>, NonZeroU16>,
317 display: MathDisplay,
318) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>>
319where
320 'config: 'source,
321 'source: 'arena,
322{
323 let style = match display {
324 MathDisplay::Inline => Style::Text,
325 MathDisplay::Block => Style::Display,
326 };
327 let lexer = Lexer::new(latex, false, cmd_cfg);
328 let mut p = Parser::new(lexer, arena, equation_count, label_map, style)?;
329 let nodes = p.parse()?;
330 Ok(nodes)
331}
332
333fn parse_custom_commands(
334 cfg: MathCoreConfig,
335) -> Result<CommandConfig, (Box<LatexError>, usize, String)> {
336 let macros = cfg.macros;
337 let mut map = FxHashMap::with_capacity_and_hasher(macros.len(), FxBuildHasher);
338 let mut tokens = Vec::new();
339 for (idx, (name, definition)) in macros.into_iter().enumerate() {
340 if !is_valid_macro_name(name.as_str()) {
341 return Err((
342 Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
343 idx,
344 definition,
345 ));
346 }
347
348 let value = 'value: {
352 let mut lexer: Lexer<'static, '_> = Lexer::new(definition.as_str(), true, None);
353 let start = tokens.len();
354 loop {
355 match lexer.next_token_no_unknown_command() {
356 Ok(tokloc) => {
357 if matches!(tokloc.token(), Token::Eoi) {
358 break;
359 }
360 tokens.push(tokloc.into_token());
361 }
362 Err(err) => {
363 break 'value Err(err);
364 }
365 }
366 }
367 let end = tokens.len();
368 let num_args = lexer.parse_cmd_args().unwrap_or(0);
369 Ok((num_args, (start, end)))
370 };
371
372 match value {
373 Err(err) => {
374 return Err((err, idx, definition));
375 }
376 Ok(v) => {
377 map.insert(name, v);
378 }
379 }
380 }
381 Ok(CommandConfig {
382 custom_cmd_tokens: tokens,
383 custom_cmd_map: map,
384 ignore_unknown_commands: cfg.ignore_unknown_commands,
385 allow_unreliable_rendering: cfg.allow_unreliable_rendering,
386 })
387}
388
389fn is_valid_macro_name(s: &str) -> bool {
390 if s.is_empty() {
391 return false;
392 }
393 let mut chars = s.chars();
394 match (chars.next(), chars.next()) {
395 (Some(_), None) => true,
397 _ => s.bytes().all(|b| b.is_ascii_alphabetic()),
399 }
400}