pythonlatexpython-sphinxsubstitutionrestructuredtext

Math Latex macros to make substitutions in reStructuredText and Sphinx


Using Sphinx and reStructuredText, would it be possible to define math macros to make substitutions in a math Latex formula?

Basically, I would like to have something like

.. |bnabla| replace:: :math:`\boldsymbol{\nabla}`
.. |vv| replace:: :math:`\textbf{v}`

.. math:: \rho \mbox{D}_t |vv| = - |bnabla| p + \rho \textbf{g} + \eta |bnabla|^2 |vv|,

where |vv| is the velocity and |bnabla| is the nabla operator.

Then follow many other equations with |vv| and |bnabla|...

but it does not work at all. First the flags are not substituted in math mode and second, even if they were substituted, a :math: statement does not work in a .. math:: block. Would it be a good idea and not to complicate to change these two behaviours in Sphinx?

Another solution would be to use latex macros as in this question Creating LaTeX math macros within Sphinx but I think the final code would be simpler to read with local rst substitutions as in my example. I would like to have clear formulas that can also be read in text mode.

Moreover, I am using the MathJax extension so I cannot use the variable pngmath_latex_preamble. I could use this solution for MathJax https://stackoverflow.com/a/19268549/1779806 but it seems quite complicate and once again the code would be clearer with "local" rst substitutions.

Edit:

I realized that it would be very convenient and useful for many people to implement a mathmacro substitution directive directly in reStructuredText (i.e. in the Python package docutils). The new directive should work like this:

Definition:

.. |bnabla| mathmacro:: \boldsymbol{\nabla}
.. |vv| mathmacro:: \textbf{v}

These math macros could be included directly in text as in

|bnabla| is the nabla operator,

which should produce an inline equation like this

:math:`\boldsymbol{\nabla}` is the nabla operator.

These macros could also be included in inline equations:

This is an inline equation :math:`\vv = \bnabla f`, 

which should be equivalent of

:math:`\textbf{v} = \boldsymbol{\nabla}f`

They could also be included in block equation

.. math:: \vv = \bnabla f

which should be equivalent of

.. math:: \textbf{v} = \boldsymbol{\nabla}f.

However, I am really not familiar with how docutils works internally. I notice in particular that the MathBlock directive (defined in docutils/parsers/rst/directives/body.py) does not call any parsing of its input so there is no substitution in math mode.

I do not know if it is possible to change the behaviour of a substitution directive such that the substitution is cleaver and adapt to the context from where it is call, in text, in inline math or in block math.

Does someone can give me some clues on how to implement this useful new feature?


Solution

  • Since I really needed a good solution for this need, I worked it out myself... It took me a while and the solution is maybe not perfect, but at least it works well. I present the result since it can be useful for other people.

    I also adapted the solution as a Sphinx extension, which can be found here.

    I had to define a new substitution directive and to redefine the math directive and role. All this is done in a file mathmacro.py:

    """
    'Proof of concept' for a new reStructuredText directive *mathmacro*.
    
    Use for example with::
    
      python mathmacro.py example.rst example.html
    
    """
    
    import re
    
    from docutils.parsers.rst.directives.misc import Replace
    
    from docutils.parsers.rst.directives.body import MathBlock
    from docutils.parsers.rst.roles import math_role
    
    def multiple_replacer(replace_dict):
        """Return a function replacing doing multiple replacements.
    
        The produced function replace `replace_dict.keys()` by
        `replace_dict.values`, respectively.
    
        """
        def replacement_function(match):
            s = match.group(0)
            end = s[-1]
            if re.match(r'[\W_]', end):
                return replace_dict[s[:-1]]+end
            else:
                return replace_dict[s]
    
        pattern = re.compile("|".join([re.escape(k)+r'[\W_\Z]|'+re.escape(k)+r'\Z'
                                       for k in replace_dict.keys()]), 
                             re.M)
        return lambda string: pattern.sub(replacement_function, string)
    
    
    class MathMacro(Replace):
        """Directive defining a math macro."""
        def run(self):
            if not hasattr(self.state.document, 'math_macros'):
                self.state.document.math_macros = {}
    
            latex_key = '\\'+self.state.parent.rawsource.split('|')[1]
            self.state.document.math_macros[latex_key] = ''.join(self.content)
    
            self.state.document.math_macros_replace = \
                multiple_replacer(self.state.document.math_macros)
    
            self.content[0] = ':math:`'+self.content[0]
            self.content[-1] = self.content[-1]+'`'
    
            return super(MathMacro, self).run()
    
    
    class NewMathBlock(MathBlock):
        """New math block directive parsing the latex code."""
        def run(self):
            try:
                multiple_replace = self.state.document.math_macros_replace
            except AttributeError:
                pass
            else:
                if self.state.document.math_macros:
                    for i, c in enumerate(self.content):
                        self.content[i] = multiple_replace(c)
            return super(NewMathBlock, self).run()
    
    
    def new_math_role(role, rawtext, text, lineno, inliner, 
                      options={}, content=[]):
        """New math role parsing the latex code."""
        try:
            multiple_replace = inliner.document.math_macros_replace
        except AttributeError:
            pass
        else:
            if inliner.document.math_macros:
                rawtext = multiple_replace(rawtext)
    
        return math_role(role, rawtext, text, lineno, inliner,
                         options=options, content=content)
    
    
    if __name__ == '__main__':
    
        from docutils.parsers.rst.directives import register_directive
        from docutils.parsers.rst.roles import register_canonical_role
    
        register_directive('mathmacro', MathMacro)
        register_directive('math', NewMathBlock)
        register_canonical_role('math', new_math_role)
    
    
    
        from docutils.core import publish_cmdline, default_description
        description = ('Generates (X)HTML documents '
                       'from standalone reStructuredText sources. '
                       +default_description)
        publish_cmdline(writer_name='html', description=description)
    

    The content of the file example.rst:

    Here, I show how to use a new mathmacro substitution directive in
    reStructuredText. I think even this small example demonstrates that it
    is useful.
    
    
    First some math without math macros.  Let's take the example of the
    incompressible Navier-Stokes equation:
    
    .. math:: \mbox{D}_t \textbf{v} = 
       -\boldsymbol{\nabla} p + \nu \boldsymbol{\nabla} ^2 \textbf{v}.
    
    where :math:`\mbox{D}_t` is the convective derivative,
    :math:`\textbf{v}` the velocity, :math:`\boldsymbol{\nabla}` the
    nabla operator, :math:`\nu` the viscosity and
    :math:`\boldsymbol{\nabla}^2` the Laplacian operator.
    
    
    .. |Dt| mathmacro:: \mbox{D}_t
    .. |bnabla| mathmacro:: \boldsymbol{\nabla}
    .. |vv| mathmacro:: \textbf{v}
    
    Now, let's use some math macros and try to get the same result...  The
    Navier-Stokes equation can now be written like this:
    
    .. math:: \Dt \vv = - \bnabla p + \nu \bnabla^2 \vv
    
    where |Dt| is the convective derivative, |vv| the velocity, |bnabla|
    the nabla operator, :math:`\nu` the viscosity and :math:`\bnabla^2`
    the Laplacian operator.