use asciimath_rs::format::mathml::ToMathML as _;
use math_core::{
LatexToMathML,
MathCoreConfig,
MathDisplay,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Notation {
AsciiMath,
Latex,
}
pub(crate) fn mathml(source: &str, notation: Notation) -> Option<String> {
let source = source.trim();
if source.is_empty() {
return None;
}
convert(source, notation, MathDisplay::Block)
}
fn convert(source: &str, notation: Notation, display: MathDisplay) -> Option<String> {
let source = source.trim();
if source.is_empty() {
return None;
}
match notation {
Notation::Latex => latex(source, display),
Notation::AsciiMath => ascii(source, display),
}
}
pub(crate) fn inline(html: &str) -> String {
let mut out = String::with_capacity(html.len());
let mut rest = html;
while !rest.is_empty() {
let (prose, verbatim) = match rest.find("<pre") {
Some(at) => match rest[at..].find("</pre>") {
Some(end) => (&rest[..at], &rest[at..at + end + "</pre>".len()]),
None => (&rest[..at], &rest[at..]),
},
None => (rest, ""),
};
out.push_str(&convert_all(prose));
out.push_str(verbatim);
rest = &rest[prose.len() + verbatim.len()..];
}
out
}
fn convert_all(prose: &str) -> String {
let mut out = String::with_capacity(prose.len());
let mut rest = prose;
while let Some((at, open, close, notation)) = next_equation(rest) {
out.push_str(&rest[..at]);
let body = &rest[at + open.len()..];
let Some(end) = body.find(close) else {
out.push_str(&rest[at..at + open.len()]);
rest = &rest[at + open.len()..];
continue;
};
let source = unescape(&body[..end]);
let converted = convert(&source, notation, MathDisplay::Inline);
match converted {
Some(mathml) => out.push_str(&mathml),
None => out.push_str(&rest[at..at + open.len() + end + close.len()]),
}
rest = &body[end + close.len()..];
}
out.push_str(rest);
out
}
fn next_equation(prose: &str) -> Option<(usize, &'static str, &'static str, Notation)> {
let latex = prose
.find("\\(")
.map(|at| (at, "\\(", "\\)", Notation::Latex));
let ascii = prose
.find("\\$")
.map(|at| (at, "\\$", "\\$", Notation::AsciiMath));
match (latex, ascii) {
(Some(l), Some(a)) if a.0 < l.0 => Some(a),
(Some(l), _) => Some(l),
(None, found) => found,
}
}
fn unescape(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
}
fn latex(source: &str, display: MathDisplay) -> Option<String> {
static CONVERTER: std::sync::OnceLock<Option<LatexToMathML>> = std::sync::OnceLock::new();
let converter = CONVERTER
.get_or_init(|| LatexToMathML::new(MathCoreConfig::default()).ok())
.as_ref()?;
converter
.convert_with_local_state(source, display)
.ok()
.map(|rendered| rendered.mathml)
}
fn ascii(source: &str, display: MathDisplay) -> Option<String> {
let rendered = asciimath_rs::parse(source).to_mathml();
if rendered.trim().is_empty() {
return None;
}
let attribute = match display {
MathDisplay::Block => " display=\"block\"",
MathDisplay::Inline => "",
};
Some(format!("<math{attribute}>{rendered}</math>"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_latex() {
let out = mathml(r"C = \alpha + \beta Y^{\gamma}", Notation::Latex).expect("converts");
assert!(out.starts_with("<math"), "{out}");
assert!(out.contains("<mi>α</mi>"), "{out}");
}
#[test]
fn converts_the_shapes_worth_having() {
for source in [
r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}",
r"\int_0^x e^{-t^2}\,dt",
r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}",
r"\lim_{x \to \infty} \frac{1}{x} = 0",
] {
assert!(
mathml(source, Notation::Latex).is_some(),
"should convert: {source}"
);
}
}
#[test]
fn converts_asciimath() {
let out = mathml("a^2 + b^2 = c^2", Notation::AsciiMath).expect("converts");
assert!(out.starts_with("<math"), "{out}");
assert!(out.contains("<msup>"), "{out}");
}
#[test]
fn declines_what_it_cannot_read() {
assert!(mathml(r"\frac{", Notation::Latex).is_none());
assert!(mathml(" ", Notation::AsciiMath).is_none());
assert!(mathml("", Notation::Latex).is_none());
}
}