about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py261
1 files changed, 261 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
new file mode 100644
index 00000000..11f9ab3e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
@@ -0,0 +1,261 @@
+# :Id: $Id: tex2mathml_extern.py 9536 2024-02-01 13:04:22Z 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.
+"""
+
+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):
+    """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 <error> messages to stdout
+    if '<error>' in result.stdout:
+        result.stderr = result.stdout[result.stdout.find('<message>')+9:
+                                      result.stdout.find('</message>')]
+    else:
+        result.stdout = result.stdout[result.stdout.find('<markup>')+9:
+                                      result.stdout.find('</markup>')]
+    _check_result(result)
+    return (f'<math xmlns="http://www.w3.org/1998/Math/MathML"{mathml_args}>'
+            f'\n{result.stdout}</math>')
+
+
+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)
+
+    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:')
+                                   or line.startswith('Warning:')
+                                   or line.startswith('Fatal:'))
+    _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:
+    # <table> with <math> in cells for "align", <math> element else.
+    start = result2.stdout.find('<table class="ltx_equationgroup')
+    if start != -1:
+        stop = result2.stdout.find('</table>', start)+8
+        result2.stdout = result2.stdout[start:stop].replace(
+            'ltx_equationgroup', 'borderless align-center')
+    else:
+        result2.stdout = result2.stdout[result2.stdout.find('<math'):
+                                        result2.stdout.find('</math>')+7]
+    # Search for error messages
+    if result2.stdout:
+        _msg_source = result2.stdout  # latexmlpost reports errors in output
+    else:
+        _msg_source = result2.stderr  # just in case
+    result2.stderr = '\n'.join(line for line in _msg_source.splitlines()
+                               if line.startswith('Error:')
+                               or line.startswith('Warning:')
+                               or line.startswith('Fatal:'))
+    _check_result(result2)
+    return result2.stdout
+
+
+def pandoc(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with pandoc__.
+
+    __ https://pandoc.org/
+    """
+    args = ['pandoc',
+            '--mathml',
+            '--from=latex',
+            ]
+    result = subprocess.run(args, input=wrap_math_code(math_code, as_block),
+                            capture_output=True, text=True)
+
+    result.stdout = result.stdout[result.stdout.find('<math'):
+                                  result.stdout.find('</math>')+7]
+    # Pandoc (2.9.2.1) messages are pre-formatted for the terminal:
+    #   1. summary
+    #   2. math source (part)
+    #   3. error spot indicator '^' (works only in a literal block)
+    #   4. assumed problem
+    #   5. assumed solution (may be wrong or confusing)
+    # Construct a "details" list:
+    details = []
+    if result.stderr:
+        lines = result.stderr.splitlines()
+        details.append(nodes.paragraph('', lines[0]))
+        details.append(nodes.literal_block('', '\n'.join(lines[1:3])))
+        details.append(nodes.paragraph('', '\n'.join(lines[3:]),
+                                       classes=['pre-wrap']))
+    _check_result(result, details=details)
+    return result.stdout
+
+
+def ttm(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with TtM__.
+
+    Aged, limited, but fast.
+
+    __ http://silas.psfc.mit.edu/tth/mml/
+    """
+    args = ['ttm',
+            '-L',  # source is LaTeX snippet
+            '-r']  # output MathML snippet
+    math_code = wrap_math_code(math_code, as_block)
+
+    # "ttm" does not support UTF-8 input. (Docutils converts most math
+    # characters to LaTeX commands before calling this function.)
+    try:
+        result = subprocess.run(args, input=math_code,
+                                capture_output=True, text=True,
+                                encoding='ISO-8859-1')
+    except UnicodeEncodeError as err:
+        raise MathError(err)
+
+    result.stdout = result.stdout[result.stdout.find('<math'):
+                                  result.stdout.find('</math>')+7]
+    if as_block:
+        result.stdout = result.stdout.replace('<math xmlns=',
+                                              '<math display="block" xmlns=')
+    result.stderr = '\n'.join(line[5:] + '.'
+                              for line in result.stderr.splitlines()
+                              if line.startswith('**** '))
+    _check_result(result)
+    return result.stdout
+
+
+# self-test
+
+if __name__ == "__main__":
+    example = (r'\frac{\partial \sin^2(\alpha)}{\partial \vec r}'
+               r'\varpi \mathbb{R} \, \text{Grüße}')
+
+    print("""<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>test external mathml converters</title>
+</head>
+<body>
+<p>Test external converters</p>
+<p>
+""")
+    print(f'latexml: {latexml(example)},')
+    print(f'ttm: {ttm(example.replace("mathbb", "mathbf"))},')
+    print(f'blahtexml: {blahtexml(example)},')
+    print(f'pandoc: {pandoc(example)}.')
+    print('</p>')
+
+    print('<p>latexml:</p>')
+    print(latexml(example, as_block=True))
+    print('<p>ttm:</p>')
+    print(ttm(example.replace('mathbb', 'mathbf'), as_block=True))
+    print('<p>blahtexml:</p>')
+    print(blahtexml(example, as_block=True))
+    print('<p>pandoc:</p>')
+    print(pandoc(example, as_block=True))
+
+    print('</main>\n</body>\n</html>')
+
+    buggy = r'\sinc \phy'
+    # buggy = '\sqrt[e]'
+    try:
+        # print(blahtexml(buggy))
+        # print(latexml(f'${buggy}$'))
+        print(pandoc(f'${buggy}$'))
+        # print(ttm(f'${buggy}$'))
+    except MathError as err:
+        print(err)
+        print(err.details)
+        for node in err.details:
+            print(node.astext())