File size: 7,810 Bytes
fe41391 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
r"""
A role and directive to display mathtext in Sphinx
==================================================
The ``mathmpl`` Sphinx extension creates a mathtext image in Matplotlib and
shows it in html output. Thus, it is a true and faithful representation of what
you will see if you pass a given LaTeX string to Matplotlib (see
:ref:`mathtext`).
.. warning::
In most cases, you will likely want to use one of `Sphinx's builtin Math
extensions
<https://www.sphinx-doc.org/en/master/usage/extensions/math.html>`__
instead of this one. The builtin Sphinx math directive uses MathJax to
render mathematical expressions, and addresses accessibility concerns that
``mathmpl`` doesn't address.
Mathtext may be included in two ways:
1. Inline, using the role::
This text uses inline math: :mathmpl:`\alpha > \beta`.
which produces:
This text uses inline math: :mathmpl:`\alpha > \beta`.
2. Standalone, using the directive::
Here is some standalone math:
.. mathmpl::
\alpha > \beta
which produces:
Here is some standalone math:
.. mathmpl::
\alpha > \beta
Options
-------
The ``mathmpl`` role and directive both support the following options:
fontset : str, default: 'cm'
The font set to use when displaying math. See :rc:`mathtext.fontset`.
fontsize : float
The font size, in points. Defaults to the value from the extension
configuration option defined below.
Configuration options
---------------------
The mathtext extension has the following configuration options:
mathmpl_fontsize : float, default: 10.0
Default font size, in points.
mathmpl_srcset : list of str, default: []
Additional image sizes to generate when embedding in HTML, to support
`responsive resolution images
<https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images>`__.
The list should contain additional x-descriptors (``'1.5x'``, ``'2x'``,
etc.) to generate (1x is the default and always included.)
"""
import hashlib
from pathlib import Path
from docutils import nodes
from docutils.parsers.rst import Directive, directives
import sphinx
from sphinx.errors import ConfigError, ExtensionError
import matplotlib as mpl
from matplotlib import _api, mathtext
from matplotlib.rcsetup import validate_float_or_None
# Define LaTeX math node:
class latex_math(nodes.General, nodes.Element):
pass
def fontset_choice(arg):
return directives.choice(arg, mathtext.MathTextParser._font_type_mapping)
def math_role(role, rawtext, text, lineno, inliner,
options={}, content=[]):
i = rawtext.find('`')
latex = rawtext[i+1:-1]
node = latex_math(rawtext)
node['latex'] = latex
node['fontset'] = options.get('fontset', 'cm')
node['fontsize'] = options.get('fontsize',
setup.app.config.mathmpl_fontsize)
return [node], []
math_role.options = {'fontset': fontset_choice,
'fontsize': validate_float_or_None}
class MathDirective(Directive):
"""
The ``.. mathmpl::`` directive, as documented in the module's docstring.
"""
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {'fontset': fontset_choice,
'fontsize': validate_float_or_None}
def run(self):
latex = ''.join(self.content)
node = latex_math(self.block_text)
node['latex'] = latex
node['fontset'] = self.options.get('fontset', 'cm')
node['fontsize'] = self.options.get('fontsize',
setup.app.config.mathmpl_fontsize)
return [node]
# This uses mathtext to render the expression
def latex2png(latex, filename, fontset='cm', fontsize=10, dpi=100):
with mpl.rc_context({'mathtext.fontset': fontset, 'font.size': fontsize}):
try:
depth = mathtext.math_to_image(
f"${latex}$", filename, dpi=dpi, format="png")
except Exception:
_api.warn_external(f"Could not render math expression {latex}")
depth = 0
return depth
# LaTeX to HTML translation stuff:
def latex2html(node, source):
inline = isinstance(node.parent, nodes.TextElement)
latex = node['latex']
fontset = node['fontset']
fontsize = node['fontsize']
name = 'math-{}'.format(
hashlib.md5(f'{latex}{fontset}{fontsize}'.encode()).hexdigest()[-10:])
destdir = Path(setup.app.builder.outdir, '_images', 'mathmpl')
destdir.mkdir(parents=True, exist_ok=True)
dest = destdir / f'{name}.png'
depth = latex2png(latex, dest, fontset, fontsize=fontsize)
srcset = []
for size in setup.app.config.mathmpl_srcset:
filename = f'{name}-{size.replace(".", "_")}.png'
latex2png(latex, destdir / filename, fontset, fontsize=fontsize,
dpi=100 * float(size[:-1]))
srcset.append(
f'{setup.app.builder.imgpath}/mathmpl/{filename} {size}')
if srcset:
srcset = (f'srcset="{setup.app.builder.imgpath}/mathmpl/{name}.png, ' +
', '.join(srcset) + '" ')
if inline:
cls = ''
else:
cls = 'class="center" '
if inline and depth != 0:
style = 'style="position: relative; bottom: -%dpx"' % (depth + 1)
else:
style = ''
return (f'<img src="{setup.app.builder.imgpath}/mathmpl/{name}.png"'
f' {srcset}{cls}{style}/>')
def _config_inited(app, config):
# Check for srcset hidpi images
for i, size in enumerate(app.config.mathmpl_srcset):
if size[-1] == 'x': # "2x" = "2.0"
try:
float(size[:-1])
except ValueError:
raise ConfigError(
f'Invalid value for mathmpl_srcset parameter: {size!r}. '
'Must be a list of strings with the multiplicative '
'factor followed by an "x". e.g. ["2.0x", "1.5x"]')
else:
raise ConfigError(
f'Invalid value for mathmpl_srcset parameter: {size!r}. '
'Must be a list of strings with the multiplicative '
'factor followed by an "x". e.g. ["2.0x", "1.5x"]')
def setup(app):
setup.app = app
app.add_config_value('mathmpl_fontsize', 10.0, True)
app.add_config_value('mathmpl_srcset', [], True)
try:
app.connect('config-inited', _config_inited) # Sphinx 1.8+
except ExtensionError:
app.connect('env-updated', lambda app, env: _config_inited(app, None))
# Add visit/depart methods to HTML-Translator:
def visit_latex_math_html(self, node):
source = self.document.attributes['source']
self.body.append(latex2html(node, source))
def depart_latex_math_html(self, node):
pass
# Add visit/depart methods to LaTeX-Translator:
def visit_latex_math_latex(self, node):
inline = isinstance(node.parent, nodes.TextElement)
if inline:
self.body.append('$%s$' % node['latex'])
else:
self.body.extend(['\\begin{equation}',
node['latex'],
'\\end{equation}'])
def depart_latex_math_latex(self, node):
pass
app.add_node(latex_math,
html=(visit_latex_math_html, depart_latex_math_html),
latex=(visit_latex_math_latex, depart_latex_math_latex))
app.add_role('mathmpl', math_role)
app.add_directive('mathmpl', MathDirective)
if sphinx.version_info < (1, 8):
app.add_role('math', math_role)
app.add_directive('math', MathDirective)
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
return metadata
|