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