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 result = converter.convert_with_local_state(latex, MathDisplay::Block).unwrap();
21//! println!("{}", result.mathml);
22//! ```
23//!
24//! # Features
25//!
26//! - `std` (enabled by default): Uses the Rust standard library. Disabling this feature (with
27//! `default-features = false`) makes the crate `no_std`; the `alloc` crate is still required.
28//! Note that disabling `std` also disables some speedups in dependencies (e.g. `memchr` then
29//! can no longer use runtime CPU feature detection).
30//! - `serde`: With this feature, `MathCoreConfig` implements serde's `Serialize` and
31//! `Deserialize`.
32//! - `ariadne`: Adds `LatexError::to_report()`, which converts an error into an
33//! [`ariadne`](https://docs.rs/ariadne) report for pretty-printing the error together with a
34//! source code snippet. The `ariadne` crate itself requires `std`, so this feature is not
35//! usable on `no_std` targets.
36//!
37#![cfg_attr(not(any(feature = "std", test)), no_std)]
38
39extern crate alloc;
40
41mod atof;
42mod character_class;
43mod color_defs;
44mod commands;
45mod custom_cmds;
46mod environments;
47mod error;
48mod global_state;
49mod html_utils;
50mod lexer;
51mod parser;
52mod predefined;
53mod specifications;
54mod split_on_ascii;
55mod string_pool;
56mod text_parser;
57mod token;
58mod token_queue;
59
60use alloc::boxed::Box;
61use alloc::string::String;
62use alloc::vec::Vec;
63use core::ops::Range;
64
65use kstring::KString;
66use rustc_hash::FxBuildHasher;
67#[cfg(feature = "serde")]
68use serde::{Deserialize, Serialize};
69
70/// Hash map with a fast, non-cryptographic hasher, backed by `hashbrown` so it works in `no_std`.
71pub(crate) type FxHashMap<K, V> = hashbrown::HashMap<K, V, FxBuildHasher>;
72
73pub use mathml_renderer::ast::{CssClassNames, IndentKeyword, Indentation, Warnings};
74use mathml_renderer::{
75 arena::Arena,
76 ast::{Emitter, Node},
77 attribute::Style,
78 fmt::new_line_and_indent,
79};
80
81pub use self::error::LatexError;
82use self::{
83 commands::resolve_builtin_cmd,
84 custom_cmds::{CmdSource, CustomCmds, RecordedToken, is_valid_macro_name},
85 error::LatexErrKind,
86 global_state::GlobalState,
87 lexer::{Lexer, LexerOutput},
88 parser::Parser,
89 token::Token,
90};
91
92/// Display mode for the LaTeX math equations.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum MathDisplay {
95 /// For inline equations, like those in `$...$` in LaTeX.
96 Inline,
97 /// For block equations (or "display style" equations), like those in `$$...$$` in LaTeX.
98 Block,
99}
100
101/// Configuration for pretty-printing the MathML output.
102///
103/// Pretty-printing means that newlines and indentation is added to the MathML output, to make it
104/// easier to read.
105#[derive(Debug, Clone, Copy, Default)]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
108#[non_exhaustive]
109pub enum PrettyPrint {
110 /// Never pretty print.
111 #[default]
112 Never,
113 /// Always pretty print.
114 Always,
115 /// Pretty print for block equations only.
116 Auto,
117}
118
119/// Configuration for using Unicode symbols in the MathML output.
120///
121/// LaTeX commands like `\coloneqq` can be rendered in MathML either using dedicated Unicode symbols
122/// (in this case, `\coloneqq` would be rendered as `≔`) or using a combination of more basic
123/// symbols (in this case, `\coloneqq` would be rendered as a combination of `:` and `=`).
124/// The former is preferable in terms of semantics but can look a little different from the LaTeX
125/// output, while the latter is more faithful to the LaTeX output but can be less semantically
126/// clear.
127#[derive(Debug, Clone, Copy, Default)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
130#[non_exhaustive]
131pub enum UnicodeSubstitution {
132 /// Never subtitute a set of symbols with their Unicode equivalents.
133 Never,
134 /// Substitute whenever the LaTeX package `unicode-math` would substitute, which is a good
135 /// middle ground between semantics and faithfulness to the LaTeX output.
136 #[default]
137 Conventional,
138 // /// Substitute whenever there is a Unicode equivalent, even if the `unicode-math` package
139 // /// does not do so.
140 // Aggressive,
141}
142
143/// The maximum number of custom command expansions allowed in one snippet.
144///
145/// Names are resolved when a command is expanded, so a definition may refer to itself, directly or
146/// through other definitions, and expanding it would never end. Rather than detecting that, we
147/// simply stop after this many expansions, as LaTeX and KaTeX do.
148///
149/// The default is 1000, which is the same limit that KaTeX uses for its `maxExpand` setting.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
152pub struct MaxExpansions(pub u32);
153
154impl Default for MaxExpansions {
155 fn default() -> Self {
156 MaxExpansions(1000)
157 }
158}
159
160/// Configuration object for the LaTeX to MathML conversion.
161///
162/// # Example usage
163///
164/// ```rust
165/// use math_core::{MathCoreConfig, PrettyPrint};
166///
167/// // Default values
168/// let config = MathCoreConfig::default();
169///
170/// // Specifying pretty-print behavior
171/// let config = MathCoreConfig {
172/// pretty_print: PrettyPrint::Always,
173/// ..Default::default()
174/// };
175///
176/// // Specifying pretty-print behavior and custom macros
177/// let macros = vec![
178/// ("d".to_string(), r"\mathrm{d}".to_string()),
179/// ("bb".to_string(), r"\mathbb{#1}".to_string()), // with argument
180/// ];
181/// let config = MathCoreConfig {
182/// pretty_print: PrettyPrint::Auto,
183/// macros,
184/// ..Default::default()
185/// };
186/// ```
187///
188#[derive(Debug, Default)]
189#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
190#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
191pub struct MathCoreConfig {
192 /// A configuration for pretty-printing the MathML output. See [`PrettyPrint`] for details.
193 pub pretty_print: PrettyPrint,
194 /// A list of LaTeX macros; each tuple contains (macro_name, macro_definition).
195 ///
196 /// A macro may use another macro of this list, no matter which of the two comes first.
197 #[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
198 pub macros: Vec<(String, String)>,
199 /// If `true`, include `xmlns="http://www.w3.org/1998/Math/MathML"` in the `<math>` tag.
200 pub xml_namespace: bool,
201 /// If `true`, unknown commands will be rendered as red text in the output, instead of
202 /// returning an error.
203 pub ignore_unknown_commands: bool,
204 /// If `true`, wrap the MathML output in `<semantics>` tags with an
205 /// `<annotation encoding="application/x-tex">` child containing the original LaTeX source.
206 pub annotation: bool,
207 /// If `true`, allow rendering commands that produce MathML Core output that is unreliably
208 /// rendered by browsers.
209 pub allow_unreliable_rendering: bool,
210 /// If `true`, run the conversion in the global group, which means that commands defined at
211 /// the top level of a snippet with `\newcommand` (and related commands) stay defined for the
212 /// snippets which come after it.
213 ///
214 /// If `false` (the default), such definitions are local to the snippet which contains them.
215 /// This matches LaTeX, where constructs like `\begin{equation}` and `$$` open a local group,
216 /// and it matches the default behavior of KaTeX.
217 pub global_group: bool,
218 /// If not `UnicodeSubstitution::Never`, substitute certain LaTeX commands with their Unicode
219 /// equivalents in the MathML output.
220 pub unicode_substitution: UnicodeSubstitution,
221 /// CSS class names for various elements in the output.
222 pub css_classes: CssClassNames,
223 /// The indentation unit used when pretty-printing the MathML output. Either a number of spaces
224 /// (e.g. `2`) or the string `"tab"` for a tab character. See [`Indentation`].
225 pub indentation: Indentation,
226 /// How many custom commands may be expanded in one snippet before the conversion gives up.
227 /// See [`MaxExpansions`].
228 pub max_expansions: MaxExpansions,
229 /// Add this string to the start of every generated `id` attribute.
230 /// This is *not* URL escaped. Use the
231 /// [percent-encoding](https://crates.io/crates/percent-encoding) crate if you need to.
232 pub id_prefix: String,
233}
234
235/// Subset of `MathCoreConfig` relevant for the parser.
236#[derive(Debug, Default)]
237struct ParserConfig {
238 custom_cmds_from_cfg: CustomCmds,
239 ignore_unknown_commands: bool,
240 allow_unreliable_rendering: bool,
241 global_group: bool,
242 unicode_substitution: UnicodeSubstitution,
243 max_expansions: MaxExpansions,
244}
245
246/// Subset of `MathCoreConfig` relevant for the emitter.
247#[derive(Debug, Default)]
248struct EmitterConfig {
249 pretty_print: PrettyPrint,
250 xml_namespace: bool,
251 annotation: bool,
252 css_classes: CssClassNames,
253 indentation: Indentation,
254 id_prefix: String,
255}
256
257impl From<MathCoreConfig> for EmitterConfig {
258 fn from(config: MathCoreConfig) -> Self {
259 // FIXME: can we use a macro here to avoid repeating the field names?
260 Self {
261 pretty_print: config.pretty_print,
262 xml_namespace: config.xml_namespace,
263 annotation: config.annotation,
264 css_classes: config.css_classes,
265 indentation: config.indentation,
266 id_prefix: config.id_prefix,
267 }
268 }
269}
270
271type ParseResult<T> = Result<T, Box<LatexError>>;
272
273/// The error type returned when parsing a custom macro definition fails. Contains the parsing
274/// error, the index of the macro definition in the `macros` vector and the macro definition itself.
275pub type MacroParseError = (Box<LatexError>, usize, String);
276
277/// A converter that transforms LaTeX math equations into MathML Core.
278#[derive(Debug, Default)]
279pub struct LatexToMathML {
280 emitter_cfg: EmitterConfig,
281 state: GlobalState,
282 parser_cfg: ParserConfig,
283}
284
285impl LatexToMathML {
286 /// Create a new `LatexToMathML` converter with the given configuration.
287 ///
288 /// This function returns an error if the custom macros in the given configuration could not
289 /// be parsed. The error contains the parsing error, the macro index and the macro definition
290 /// that caused the error.
291 pub fn new(mut config: MathCoreConfig) -> Result<Self, MacroParseError> {
292 let custom_cmds = parse_custom_commands(
293 core::mem::take(&mut config.macros),
294 config.unicode_substitution,
295 config.allow_unreliable_rendering,
296 )?;
297 let parser_cfg = ParserConfig {
298 custom_cmds_from_cfg: custom_cmds,
299 ignore_unknown_commands: config.ignore_unknown_commands,
300 allow_unreliable_rendering: config.allow_unreliable_rendering,
301 global_group: config.global_group,
302 unicode_substitution: config.unicode_substitution,
303 max_expansions: config.max_expansions,
304 };
305 Ok(Self {
306 emitter_cfg: EmitterConfig::from(config),
307 state: GlobalState::default(),
308 parser_cfg,
309 })
310 }
311
312 /// Convert LaTeX to MathML with a global equation counter.
313 ///
314 /// For basic usage, see the documentation of [`Self::convert_with_local_state`].
315 ///
316 /// This conversion function maintains state, in order to count equations correctly across
317 /// different calls to this function.
318 ///
319 /// The counter can be reset with [`Self::reset_global_state`].
320 pub fn convert_with_global_state(
321 &mut self,
322 latex: &str,
323 display: MathDisplay,
324 ) -> Result<ConvertResult, Box<LatexError>> {
325 convert(
326 latex,
327 display,
328 &self.parser_cfg,
329 &mut self.state,
330 &self.emitter_cfg,
331 )
332 }
333
334 /// Convert LaTeX to MathML.
335 ///
336 /// The second argument specifies whether it is inline-equation or block-equation.
337 ///
338 /// ```rust
339 /// use math_core::{LatexToMathML, MathCoreConfig, MathDisplay};
340 ///
341 /// let latex = r#"(n + 1)! = \Gamma ( n + 1 )"#;
342 /// let config = MathCoreConfig::default();
343 /// let converter = LatexToMathML::new(config).unwrap();
344 /// let result = converter.convert_with_local_state(latex, MathDisplay::Inline).unwrap();
345 /// println!("{}", result.mathml);
346 ///
347 /// let latex = r#"x = \frac{ - b \pm \sqrt{ b^2 - 4 a c } }{ 2 a }"#;
348 /// let result = converter.convert_with_local_state(latex, MathDisplay::Block).unwrap();
349 /// println!("{}", result.mathml);
350 /// ```
351 ///
352 pub fn convert_with_local_state(
353 &self,
354 latex: &str,
355 display: MathDisplay,
356 ) -> Result<ConvertResult, Box<LatexError>> {
357 let mut state = GlobalState::default();
358 convert(
359 latex,
360 display,
361 &self.parser_cfg,
362 &mut state,
363 &self.emitter_cfg,
364 )
365 }
366
367 /// Reset the equation counter, the label map and the commands defined with `\newcommand`.
368 ///
369 /// This should normally be done at the beginning of a new document or section.
370 pub fn reset_global_state(&mut self) {
371 self.state.equation_count = 0;
372 self.state.label_map.clear();
373 self.state.custom_cmds.clear();
374 }
375
376 /// Convert a collection of LaTeX snippets to MathML.
377 ///
378 /// This method handles *forward references* correctly, meaning that if an earlier snippet
379 /// contains a reference to an equation in a later snippet, the reference will be resolved
380 /// correctly. However, in order to achieve this, all snippets need to be parsed first and can
381 /// only then be emitted. This means you have to first extract all LaTeX snippets from your
382 /// document and then call this method with the whole set.
383 pub fn convert_all<S: AsRef<str>>(
384 &self,
385 snippets: &[(S, MathDisplay)],
386 ) -> Vec<Result<ConvertResult, Box<LatexError>>> {
387 let mut state = GlobalState::default();
388 let arena = Arena::new();
389 let ast_vec: Vec<ParseResult<(Vec<&Node<'_>>, &str, MathDisplay)>> = snippets
390 .iter()
391 .map(|(latex, display)| {
392 let latex = latex.as_ref();
393 parse(latex, &arena, &self.parser_cfg, &mut state, *display)
394 .map(|ast| (ast, latex, *display))
395 })
396 .collect::<Vec<_>>();
397 ast_vec
398 .into_iter()
399 .map(|ast_result| {
400 ast_result.map(|(ast, latex, display)| {
401 emit(
402 ast,
403 latex,
404 display,
405 &state.label_map,
406 &arena,
407 &self.emitter_cfg,
408 )
409 })
410 })
411 .collect()
412 }
413}
414
415fn convert(
416 latex: &str,
417 display: MathDisplay,
418 parser_cfg: &ParserConfig,
419 state: &mut GlobalState,
420 flags: &EmitterConfig,
421) -> Result<ConvertResult, Box<LatexError>> {
422 let arena = Arena::new();
423 let ast = parse(latex, &arena, parser_cfg, state, display)?;
424 Ok(emit(ast, latex, display, &state.label_map, &arena, flags))
425}
426
427fn emit(
428 ast: Vec<&Node>,
429 latex: &str,
430 display: MathDisplay,
431 label_map: &FxHashMap<KString, KString>,
432 arena: &Arena,
433 flags: &EmitterConfig,
434) -> ConvertResult {
435 let mut output = String::new();
436 output.push_str("<math");
437 if flags.xml_namespace {
438 output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
439 }
440 if matches!(display, MathDisplay::Block) {
441 output.push_str(" display=\"block\"");
442 }
443 output.push('>');
444
445 let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
446 || (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
447
448 let base_indent = if pretty_print { 1 } else { 0 };
449 let warnings: Warnings;
450 if flags.annotation {
451 let children_indent = if pretty_print { 2 } else { 0 };
452 new_line_and_indent(&mut output, base_indent, flags.indentation);
453 output.push_str("<semantics>");
454 let node = parser::node_vec_to_node(arena, &ast, false);
455 let mut emitter = Emitter::new(
456 core::mem::take(&mut output),
457 label_map,
458 &flags.css_classes,
459 flags.indentation,
460 &flags.id_prefix,
461 );
462 let _ = emitter.emit(node, children_indent);
463 warnings = emitter.warnings();
464 output = emitter.into_string();
465 new_line_and_indent(&mut output, children_indent, flags.indentation);
466 output.push_str("<annotation encoding=\"application/x-tex\">");
467 html_utils::escape_html_content(&mut output, latex);
468 output.push_str("</annotation>");
469 new_line_and_indent(&mut output, base_indent, flags.indentation);
470 output.push_str("</semantics>");
471 } else {
472 let mut emitter = Emitter::new(
473 core::mem::take(&mut output),
474 label_map,
475 &flags.css_classes,
476 flags.indentation,
477 &flags.id_prefix,
478 );
479 for node in ast {
480 // We ignore the result of `emit` here, because the only possible error is a formatting
481 // error when writing to the string, but `String`'s `write_str` implementation never
482 // returns an error.
483 let _ = emitter.emit(node, base_indent);
484 }
485 warnings = emitter.warnings();
486 output = emitter.into_string();
487 }
488 if pretty_print {
489 output.push('\n');
490 }
491 output.push_str("</math>");
492 ConvertResult {
493 mathml: output,
494 warnings,
495 }
496}
497
498/// The result of a LaTeX to MathML conversion.
499pub struct ConvertResult {
500 pub mathml: String,
501 pub warnings: Warnings,
502}
503
504fn parse<'arena>(
505 latex: &'arena str,
506 arena: &'arena Arena,
507 parser_cfg: &'arena ParserConfig,
508 state: &mut GlobalState,
509 display: MathDisplay,
510) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>> {
511 let style = match display {
512 MathDisplay::Inline => Style::Text,
513 MathDisplay::Block => Style::Display,
514 };
515 let lexer = Lexer::new(latex);
516 let mut p = Parser::new(lexer, arena, parser_cfg, state, style)?;
517 let nodes = p.parse()?;
518 Ok(nodes)
519}
520
521/// Read the macros of the configuration into a store of custom commands.
522///
523/// As in a body recorded from a `\newcommand`, every command is kept as a
524/// [`RecordedToken::CommandName`] and only resolved when the macro is used. A macro may
525/// therefore refer to a command which is not defined here at all, and in particular to another
526/// macro of the configuration, no matter in which order the two are given. Once all macros have
527/// been read, every one of those references must point at something, which is what the final
528/// check is for; a document definition, which has no such point in time, is only checked when
529/// it is used.
530fn parse_custom_commands(
531 macros: Vec<(String, String)>,
532 unicode_substitution: UnicodeSubstitution,
533 allow_unreliable_rendering: bool,
534) -> Result<CustomCmds, MacroParseError> {
535 let mut custom_cmds = CustomCmds::with_capacity(macros.len());
536 // The names which have to be defined by the time all macros have been read, together with
537 // the macro they appear in and their position within its definition.
538 let mut unresolved: Vec<(usize, KString, Range<usize>)> = Vec::new();
539 let mut body = Vec::new();
540 let parser_cfg = ParserConfig {
541 unicode_substitution,
542 allow_unreliable_rendering,
543 ..Default::default()
544 };
545 // The definitions are kept around, because the check at the end has to be able to report
546 // the one which contains an unresolved name.
547 let mut definitions: Vec<String> = Vec::with_capacity(macros.len());
548 for (idx, (name, definition)) in macros.into_iter().enumerate() {
549 if !is_valid_macro_name(name.as_str()) {
550 return Err((
551 Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
552 idx,
553 definition,
554 ));
555 }
556
557 body.clear();
558 let mut num_args = 0;
559 let mut first_class: Option<character_class::Class> = None;
560 let result = 'body: {
561 let mut lexer = Lexer::new(definition.as_str());
562 loop {
563 match lexer.next_token() {
564 Ok(lexer_output) => {
565 let token = match lexer_output {
566 LexerOutput::CommandName(cmd_name, span) => {
567 // We resolve the command here only to know whether it *can* be
568 // resolved and to know its class. The actual resolution is done
569 // when the macro is used.
570 if let Some(resolved) = resolve_builtin_cmd(&parser_cfg, cmd_name) {
571 if first_class.is_none() {
572 first_class = resolved.class();
573 }
574 } else {
575 unresolved.push((
576 idx,
577 KString::from_ref(cmd_name),
578 span.into(),
579 ));
580 }
581 body.push(RecordedToken::CommandName(KString::from_ref(cmd_name)));
582 continue;
583 }
584 LexerOutput::Token(tokspan) => tokspan.into_token(),
585 };
586 match token {
587 Token::Eoi => break,
588 Token::CustomCmdArgInput(n) => {
589 if n >= num_args {
590 num_args = n + 1;
591 }
592 body.push(RecordedToken::Token(Token::CustomCmdArg(n)));
593 }
594 tok => {
595 if first_class.is_none() {
596 first_class = tok.class();
597 }
598 body.push(RecordedToken::Token(tok))
599 }
600 }
601 }
602 Err(err) => {
603 break 'body Err(err);
604 }
605 }
606 }
607 Ok(())
608 };
609
610 if let Err(err) = result {
611 return Err((err, idx, definition));
612 }
613 custom_cmds.insert(name.as_str(), num_args, &body, first_class);
614 // The lexer, which borrows the definition, is gone by now.
615 definitions.push(definition);
616 }
617 // Now that all macros are known, every name which none of them defines is an error.
618 for (idx, name, span) in unresolved {
619 if custom_cmds.get(&name, CmdSource::Config).is_none() {
620 let err = Box::new(LatexError(span, LatexErrKind::UnknownCommand(name)));
621 return Err((err, idx, definitions.swap_remove(idx)));
622 }
623 }
624 Ok(custom_cmds)
625}