# :Id: $Id: tex2mathml_extern.py 10136 2025-05-20 15:48:27Z milde $
# :Copyright: © 2015 Günter Milde.
# :License: Released under the terms of the `2-Clause BSD license`__, in short:
#
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.
# This file is offered as-is, without any warranty.
#
# __ https://opensource.org/licenses/BSD-2-Clause
"""Wrappers for TeX->MathML conversion by external tools
This module is provisional:
the API is not settled and may change with any minor Docutils version.
"""
from __future__ import annotations
__docformat__ = 'reStructuredText'
import subprocess
from docutils import nodes
from docutils.utils.math import MathError, wrap_math_code
# `latexml` expects a complete document:
document_template = r"""\documentclass{article}
\begin{document}
%s
\end{document}
"""
def _check_result(result, details=[]):
# raise MathError if the conversion went wrong
# :details: list of doctree nodes with additional info
msg = ''
if not details and result.stderr:
details = [nodes.paragraph('', result.stderr, classes=['pre-wrap'])]
if details:
msg = f'TeX to MathML converter `{result.args[0]}` failed:'
elif result.returncode:
msg = (f'TeX to MathMl converter `{result.args[0]}` '
f'exited with Errno {result.returncode}.')
elif not result.stdout:
msg = f'TeX to MathML converter `{result.args[0]}` returned no MathML.'
if msg:
raise MathError(msg, details=details)
def blahtexml(math_code, as_block=False) -> str:
"""Convert LaTeX math code to MathML with blahtexml__.
__ http://gva.noekeon.org/blahtexml/
"""
args = ['blahtexml',
'--mathml',
'--indented',
'--spacing', 'moderate',
'--mathml-encoding', 'raw',
'--other-encoding', 'raw',
'--doctype-xhtml+mathml',
'--annotate-TeX',
]
# "blahtexml" expects LaTeX code without math-mode-switch.
# We still need to tell it about displayed equation(s).
mathml_args = ' display="block"' if as_block else ''
_wrapped = wrap_math_code(math_code, as_block)
if '{align*}' in _wrapped:
math_code = _wrapped.replace('{align*}', '{aligned}')
result = subprocess.run(args, input=math_code,
capture_output=True, text=True)
# blahtexml writes messages to stdout
if '' in result.stdout:
result.stderr = result.stdout[result.stdout.find('')+9:
result.stdout.find('')]
else:
result.stdout = result.stdout[result.stdout.find('')+9:
result.stdout.find('')]
_check_result(result)
return (f'')
def latexml(math_code, as_block=False):
"""Convert LaTeX math code to MathML with LaTeXML__.
Comprehensive macro support but **very** slow.
__ http://dlmf.nist.gov/LaTeXML/
"""
# LaTeXML works in 2 stages, expects complete documents.
#
# The `latexmlmath`__ convenience wrapper does not support block-level
# (displayed) equations.
#
# __ https://metacpan.org/dist/LaTeXML/view/bin/latexmlmath
args1 = ['latexml',
'-', # read from stdin
'--preload=amsmath',
'--preload=amssymb', # also loads amsfonts
'--inputencoding=utf8',
'--',
]
math_code = document_template % wrap_math_code(math_code, as_block)
error_tags = ('Error:', 'Warning:', 'Fatal:')
result1 = subprocess.run(args1, input=math_code,
capture_output=True, text=True)
if result1.stderr:
result1.stderr = '\n'.join(line for line in result1.stderr.splitlines()
if line.startswith(error_tags))
_check_result(result1)
args2 = ['latexmlpost',
'-',
'--nonumbersections',
'--format=html5', # maths included as MathML
'--omitdoctype', # Make it simple, we only need the maths.
'--noscan', # ...
'--nocrossref',
'--nographicimages',
'--nopictureimages',
'--nodefaultresources', # do not copy *.css files to output dir
'--'
]
result2 = subprocess.run(args2, input=result1.stdout,
capture_output=True, text=True)
# Extract MathML from HTML document:
# with