#![cfg_attr(not(any(feature = "std", test)), no_std)]
extern crate alloc;
mod atof;
mod character_class;
mod color_defs;
mod commands;
mod custom_cmds;
mod environments;
mod error;
mod global_state;
mod html_utils;
mod lexer;
mod parser;
mod predefined;
mod specifications;
mod split_on_ascii;
mod string_pool;
mod text_parser;
mod token;
mod token_queue;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::ops::Range;
use kstring::KString;
use rustc_hash::FxBuildHasher;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub(crate) type FxHashMap<K, V> = hashbrown::HashMap<K, V, FxBuildHasher>;
pub use mathml_renderer::ast::{CssClassNames, IndentKeyword, Indentation, Warnings};
use mathml_renderer::{
arena::Arena,
ast::{Emitter, Node},
attribute::Style,
fmt::new_line_and_indent,
};
pub use self::error::LatexError;
use self::{
commands::resolve_builtin_cmd,
custom_cmds::{CmdSource, CustomCmds, RecordedToken, is_valid_macro_name},
error::LatexErrKind,
global_state::GlobalState,
lexer::{Lexer, LexerOutput},
parser::Parser,
token::Token,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MathDisplay {
Inline,
Block,
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[non_exhaustive]
pub enum PrettyPrint {
#[default]
Never,
Always,
Auto,
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[non_exhaustive]
pub enum UnicodeSubstitution {
Never,
#[default]
Conventional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct MaxExpansions(pub u32);
impl Default for MaxExpansions {
fn default() -> Self {
MaxExpansions(1000)
}
}
#[derive(Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
pub struct MathCoreConfig {
pub pretty_print: PrettyPrint,
#[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
pub macros: Vec<(String, String)>,
pub xml_namespace: bool,
pub ignore_unknown_commands: bool,
pub annotation: bool,
pub allow_unreliable_rendering: bool,
pub global_group: bool,
pub unicode_substitution: UnicodeSubstitution,
pub css_classes: CssClassNames,
pub indentation: Indentation,
pub max_expansions: MaxExpansions,
pub id_prefix: String,
}
#[derive(Debug, Default)]
struct ParserConfig {
custom_cmds_from_cfg: CustomCmds,
ignore_unknown_commands: bool,
allow_unreliable_rendering: bool,
global_group: bool,
unicode_substitution: UnicodeSubstitution,
max_expansions: MaxExpansions,
}
#[derive(Debug, Default)]
struct EmitterConfig {
pretty_print: PrettyPrint,
xml_namespace: bool,
annotation: bool,
css_classes: CssClassNames,
indentation: Indentation,
id_prefix: String,
}
impl From<MathCoreConfig> for EmitterConfig {
fn from(config: MathCoreConfig) -> Self {
Self {
pretty_print: config.pretty_print,
xml_namespace: config.xml_namespace,
annotation: config.annotation,
css_classes: config.css_classes,
indentation: config.indentation,
id_prefix: config.id_prefix,
}
}
}
type ParseResult<T> = Result<T, Box<LatexError>>;
pub type MacroParseError = (Box<LatexError>, usize, String);
#[derive(Debug, Default)]
pub struct LatexToMathML {
emitter_cfg: EmitterConfig,
state: GlobalState,
parser_cfg: ParserConfig,
}
impl LatexToMathML {
pub fn new(mut config: MathCoreConfig) -> Result<Self, MacroParseError> {
let custom_cmds = parse_custom_commands(
core::mem::take(&mut config.macros),
config.unicode_substitution,
config.allow_unreliable_rendering,
)?;
let parser_cfg = ParserConfig {
custom_cmds_from_cfg: custom_cmds,
ignore_unknown_commands: config.ignore_unknown_commands,
allow_unreliable_rendering: config.allow_unreliable_rendering,
global_group: config.global_group,
unicode_substitution: config.unicode_substitution,
max_expansions: config.max_expansions,
};
Ok(Self {
emitter_cfg: EmitterConfig::from(config),
state: GlobalState::default(),
parser_cfg,
})
}
pub fn convert_with_global_state(
&mut self,
latex: &str,
display: MathDisplay,
) -> Result<ConvertResult, Box<LatexError>> {
convert(
latex,
display,
&self.parser_cfg,
&mut self.state,
&self.emitter_cfg,
)
}
pub fn convert_with_local_state(
&self,
latex: &str,
display: MathDisplay,
) -> Result<ConvertResult, Box<LatexError>> {
let mut state = GlobalState::default();
convert(
latex,
display,
&self.parser_cfg,
&mut state,
&self.emitter_cfg,
)
}
pub fn reset_global_state(&mut self) {
self.state.equation_count = 0;
self.state.label_map.clear();
self.state.custom_cmds.clear();
}
pub fn convert_all<S: AsRef<str>>(
&self,
snippets: &[(S, MathDisplay)],
) -> Vec<Result<ConvertResult, Box<LatexError>>> {
let mut state = GlobalState::default();
let arena = Arena::new();
let ast_vec: Vec<ParseResult<(Vec<&Node<'_>>, &str, MathDisplay)>> = snippets
.iter()
.map(|(latex, display)| {
let latex = latex.as_ref();
parse(latex, &arena, &self.parser_cfg, &mut state, *display)
.map(|ast| (ast, latex, *display))
})
.collect::<Vec<_>>();
ast_vec
.into_iter()
.map(|ast_result| {
ast_result.map(|(ast, latex, display)| {
emit(
ast,
latex,
display,
&state.label_map,
&arena,
&self.emitter_cfg,
)
})
})
.collect()
}
}
fn convert(
latex: &str,
display: MathDisplay,
parser_cfg: &ParserConfig,
state: &mut GlobalState,
flags: &EmitterConfig,
) -> Result<ConvertResult, Box<LatexError>> {
let arena = Arena::new();
let ast = parse(latex, &arena, parser_cfg, state, display)?;
Ok(emit(ast, latex, display, &state.label_map, &arena, flags))
}
fn emit(
ast: Vec<&Node>,
latex: &str,
display: MathDisplay,
label_map: &FxHashMap<KString, KString>,
arena: &Arena,
flags: &EmitterConfig,
) -> ConvertResult {
let mut output = String::new();
output.push_str("<math");
if flags.xml_namespace {
output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
}
if matches!(display, MathDisplay::Block) {
output.push_str(" display=\"block\"");
}
output.push('>');
let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
|| (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
let base_indent = if pretty_print { 1 } else { 0 };
let warnings: Warnings;
if flags.annotation {
let children_indent = if pretty_print { 2 } else { 0 };
new_line_and_indent(&mut output, base_indent, flags.indentation);
output.push_str("<semantics>");
let node = parser::node_vec_to_node(arena, &ast, false);
let mut emitter = Emitter::new(
core::mem::take(&mut output),
label_map,
&flags.css_classes,
flags.indentation,
&flags.id_prefix,
);
let _ = emitter.emit(node, children_indent);
warnings = emitter.warnings();
output = emitter.into_string();
new_line_and_indent(&mut output, children_indent, flags.indentation);
output.push_str("<annotation encoding=\"application/x-tex\">");
html_utils::escape_html_content(&mut output, latex);
output.push_str("</annotation>");
new_line_and_indent(&mut output, base_indent, flags.indentation);
output.push_str("</semantics>");
} else {
let mut emitter = Emitter::new(
core::mem::take(&mut output),
label_map,
&flags.css_classes,
flags.indentation,
&flags.id_prefix,
);
for node in ast {
let _ = emitter.emit(node, base_indent);
}
warnings = emitter.warnings();
output = emitter.into_string();
}
if pretty_print {
output.push('\n');
}
output.push_str("</math>");
ConvertResult {
mathml: output,
warnings,
}
}
pub struct ConvertResult {
pub mathml: String,
pub warnings: Warnings,
}
fn parse<'arena>(
latex: &'arena str,
arena: &'arena Arena,
parser_cfg: &'arena ParserConfig,
state: &mut GlobalState,
display: MathDisplay,
) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>> {
let style = match display {
MathDisplay::Inline => Style::Text,
MathDisplay::Block => Style::Display,
};
let lexer = Lexer::new(latex);
let mut p = Parser::new(lexer, arena, parser_cfg, state, style)?;
let nodes = p.parse()?;
Ok(nodes)
}
fn parse_custom_commands(
macros: Vec<(String, String)>,
unicode_substitution: UnicodeSubstitution,
allow_unreliable_rendering: bool,
) -> Result<CustomCmds, MacroParseError> {
let mut custom_cmds = CustomCmds::with_capacity(macros.len());
let mut unresolved: Vec<(usize, KString, Range<usize>)> = Vec::new();
let mut body = Vec::new();
let parser_cfg = ParserConfig {
unicode_substitution,
allow_unreliable_rendering,
..Default::default()
};
let mut definitions: Vec<String> = Vec::with_capacity(macros.len());
for (idx, (name, definition)) in macros.into_iter().enumerate() {
if !is_valid_macro_name(name.as_str()) {
return Err((
Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
idx,
definition,
));
}
body.clear();
let mut num_args = 0;
let mut first_class: Option<character_class::Class> = None;
let result = 'body: {
let mut lexer = Lexer::new(definition.as_str());
loop {
match lexer.next_token() {
Ok(lexer_output) => {
let token = match lexer_output {
LexerOutput::CommandName(cmd_name, span) => {
if let Some(resolved) = resolve_builtin_cmd(&parser_cfg, cmd_name) {
if first_class.is_none() {
first_class = resolved.class();
}
} else {
unresolved.push((
idx,
KString::from_ref(cmd_name),
span.into(),
));
}
body.push(RecordedToken::CommandName(KString::from_ref(cmd_name)));
continue;
}
LexerOutput::Token(tokspan) => tokspan.into_token(),
};
match token {
Token::Eoi => break,
Token::CustomCmdArgInput(n) => {
if n >= num_args {
num_args = n + 1;
}
body.push(RecordedToken::Token(Token::CustomCmdArg(n)));
}
tok => {
if first_class.is_none() {
first_class = tok.class();
}
body.push(RecordedToken::Token(tok))
}
}
}
Err(err) => {
break 'body Err(err);
}
}
}
Ok(())
};
if let Err(err) = result {
return Err((err, idx, definition));
}
custom_cmds.insert(name.as_str(), num_args, &body, first_class);
definitions.push(definition);
}
for (idx, name, span) in unresolved {
if custom_cmds.get(&name, CmdSource::Config).is_none() {
let err = Box::new(LatexError(span, LatexErrKind::UnknownCommand(name)));
return Err((err, idx, definitions.swap_remove(idx)));
}
}
Ok(custom_cmds)
}