# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018 Philippe Faist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

r"""
A simplistic, heuristic LaTeX code parser allowing to returns a text-only
approximation.  Suitable, e.g. for indexing tex code in a database for full text
searching.

The main class is :py:class:`LatexNodes2Text`.  For a quick start, try::

    from pylatexenc.latex2text import LatexNodes2Text

    latex = "... LaTeX code ..."
    text = LatexNodes2Text().latex_to_text(latex)

You may also use the command-line version of `latex2text`::

    $ echo '\textit{italic} \`acc\^ented text' | latex2text
    italic àccênted text

"""

from __future__ import print_function, unicode_literals  # , absolute_import

import inspect
import logging
import os
import re
import sys
import textwrap


def unicode(string):
    return string


basestring = str
getfullargspec = inspect.getfullargspec

from .. import _util, latexwalker, macrospec

logger = logging.getLogger(__name__)


class MacroTextSpec(object):
    """
    A specification of how to obtain a textual representation of a macro.

    .. py:attribute:: macroname

       The name of the macro (no backslash)

    .. py:attribute:: simplify_repl

       The replacement text of the macro invocation.  This is either a string or
       a callable:

         - If `simplify_repl` is a string, this string is used as the text
           representation of this macro node.

           The string may contain a single '%s' replacement placeholder which
           will be replaced by the concatenated textual representation of all
           macro arguments.  Alternatively, the string may contain '%(<n>)s'
           (where `<n>` is an integer) to refer to the n-th argument (starting
           at '%(1)s').  You cannot mix the two %-formatting styles.

         - If `simplify_repl` is a callable, it should accept the corresponding
           :py:class:`pylatexenc.latexwalker.LatexMacroNode` as an argument.

           The callable will be inspected to see what other arguments it
           accepts.  If it accepts an argument named `l2tobj`, the
           :py:class:`LatexNodes2Text` instance is provided to that argument.
           If it accepts an argument named `macroname`, the name of the macro is
           provided to that argument.

    .. py:attribute:: discard

       If set to `True`, then the macro call is discarded, i.e., it is converted
       to an empty string.


    .. versionadded:: 2.0

       The class :py:class:`MacroTextSpec` was introduced in `pylatexenc
       2.0` to succeed to the previously named `MacroDef` class.
    """

    def __init__(self, macroname, simplify_repl=None, discard=None):
        super(MacroTextSpec, self).__init__()
        self.macroname = macroname
        self.discard = True if (discard is None) else discard
        self.simplify_repl = simplify_repl


class EnvironmentTextSpec(object):
    """
    A specification of how to obtain a textual representation of an environment.

    .. py:attribute:: environmentname

       The name of the environment

    .. py:attribute:: simplify_repl

       The replacement text of the environment.  This is either a string or a
       callable:

         - If `simplify_repl` is a string, this string is used as the text
           representation of this environment node.

           The string may contain a single '%s' replacement placeholder, in
           which the (processed) environment body will be substituted.

           Alternatively, the `simplify_repl` string may contain '%(<n>)s'
           (where `<n>` is an integer) to refer to the n-th argument after
           ``\begin{environment}`` (starting at '%(1)s').  The body of the
           environment has to be referred to with `%(body)s`.

           You cannot mix the two %-formatting styles.

         - If `simplify_repl` is a callable, it should accept the corresponding
           :py:class:`pylatexenc.latexwalker.LatexEnvironmentNode` as an
           argument.

           The callable will be inspected to see what other arguments it
           accepts.  If it accepts an argument named `l2tobj`, the
           :py:class:`LatexNodes2Text` instance is provided to that argument.
           If it accepts an argument named `environmentname`, the name of the
           environment is provided to that argument.

    .. py:attribute:: discard

       If set to `True`, then the full environment is discarded, i.e., it is
       converted to an empty string.


    .. versionadded:: 2.0

       The class :py:class:`EnvironmentTextSpec` was introduced in `pylatexenc
       2.0` to succeed to the previously named `EnvDef` class.
    """

    def __init__(self, environmentname, simplify_repl=None, discard=False):
        super(EnvironmentTextSpec, self).__init__()
        self.environmentname = environmentname
        self.simplify_repl = simplify_repl
        self.discard = discard


class SpecialsTextSpec(object):
    """
    A specification of how to obtain a textual representation of latex specials.

    .. py:attribute:: specials_chars

       The sequence of special LaTeX characters

    .. py:attribute:: simplify_repl

       The replacement text for the given latex specials.  This is either a
       string or a callable:

         - If `simplify_repl` is a string, this string is used as the text
           representation of this specials node.

           The string may contain a single '%s' replacement placeholder which
           will be replaced by the concatenated textual representation of all
           macro arguments.

           Alternatively, the string may contain '%(<n>)s' (where `<n>` is an
           integer) to refer to the n-th argument (starting at '%(1)s').  You
           cannot mix the two %-formatting styles.

         - If `simplify_repl` is a callable, it should accept the corresponding
           :py:class:`pylatexenc.latexwalker.LatexSpecialsNode` as an argument.

           The callable will be inspected to see what other arguments it
           accepts.  If it accepts an argument named `l2tobj`, the
           :py:class:`LatexNodes2Text` instance is provided to that argument.
           If it accepts an argument named `specials_chars`, the characters that
           were parsed this "latex specials" node are provided to that argument.

    .. versionadded:: 2.0

       Latex specials were introduced in `pylatexenc 2.0`.
    """

    def __init__(self, specials_chars, simplify_repl=None):
        super(SpecialsTextSpec, self).__init__()
        self.specials_chars = specials_chars
        self.simplify_repl = simplify_repl


def EnvDef(envname, simplify_repl=None, discard=False):
    r"""
    .. deprecated:: 2.0

       Instantiate a :py:class:`EnvironmentTextSpec` instead.

       Since `pylatexenc 2.0`, `EnvDef` is a function which returns a
       :py:class:`~pylatexenc.macrospec.EnvironmentTextSpec` instance.  In this
       way the earlier idiom ``EnvDef(...)`` still works in `pylatexenc 2`.
    """
    e = EnvironmentTextSpec(
        environmentname=envname, simplify_repl=simplify_repl, discard=discard
    )
    e.envname = e.environmentname
    return e


def MacroDef(macname, simplify_repl=None, discard=None):
    r"""
    .. deprecated:: 2.0

       Instantiate a :py:class:`MacroTextSpec` instead.

       Since `pylatexenc 2.0`, `MacroDef` is a function which returns a
       :py:class:`~pylatexenc.macrospec.MacroTextSpec` instance.  In this way
       the earlier idiom ``MacroDef(...)`` still works in `pylatexenc 2`.
    """
    m = MacroTextSpec(macroname=macname, simplify_repl=simplify_repl, discard=discard)
    m.macname = m.macroname
    return m


#
# NOTE: while internally documented (but not in the public docs), these fmt_***
# functions should be considered internal API and should not be relied upon for
# the moment in production code.  I intend to change some things in how common
# rendering procedures (for equations, for non-textual content that probably
# requires a placeholder, etc.) by some other means such as by extending the
# latex context database object directly.
#


def fmt_equation_environment(envnode, l2tobj):
    r"""
    Can be used as callback for display equation environments.

    .. versionadded:: 2.0

       This function was introduced in `pylatexenc 2.0`.
    """

    return l2tobj.math_node_to_text(envnode)


def fmt_input_macro(macronode, l2tobj):
    r"""
    This function can be used as callback in :py:class:`MacroTextSpec` for
    ``\input`` or ``\include`` macros.  The `macronode` must be a macro node
    with a single argument.  If :py:meth:`set_tex_input_directory()` was called
    with a nonempty input directory in the :py:class:`LatexNodes2Text` object,
    then this method reads the contents of the file name in the macro argument
    according to the provided settings.  Otherwise, returns an empty string.

    .. versionadded:: 2.0

       This function was introduced in `pylatexenc 2.0`.
    """
    return l2tobj._input_node_simplify_repl(macronode)


def placeholder_node_formatter(placeholdertext, block=True):
    r"""
    This function returns a callable that can be used in
    :py:class:`MacroTextSpec`, :py:class:`EnvironmentTextSpec`, or
    :py:class:`SpecialsTextSpec` for latex nodes that do not have a good textual
    representation, providing as text replacement the simple placeholder text
    ``'< P L A C E H O L D E R   T E X T >'``.

    If `block=True` (the default), the placeholder text is typeset in an
    indented block on its own.  Otherwise, it is typeset inline.

    .. versionadded:: 2.0

       This function was introduced in `pylatexenc 2.0`.
    """
    return lambda n, l2tobj, pht=placeholdertext: _do_fmt_placeholder_node(
        pht, l2tobj, block=block
    )


def _do_fmt_placeholder_node(placeholdertext, l2tobj, block=True):
    # spaces added so that database indexing doesn't index the word "array" or
    # "pmatrix"
    txt = "< " + " ".join(placeholdertext) + " >"
    if block:
        return l2tobj._fmt_indented_block(txt, indent="    ")
    return " " + txt + " "


def fmt_placeholder_node(node, l2tobj):
    r"""
    This function can be used as callable in :py:class:`MacroTextSpec`,
    :py:class:`EnvironmentTextSpec`, or :py:class:`SpecialsTextSpec` for latex
    nodes that do not have a good textual representation.  The text replacement
    is the placeholder text
    ``'< N A M E   O F   T H E   M A C R O   O R   E N V I R O N M E N T >'``.

    .. versionadded:: 2.0

       This function was introduced in `pylatexenc 2.0`.
    """

    for att in ("macroname", "environmentname", "specials_chars"):
        if hasattr(node, att):
            name = getattr(node, att)
            break
    else:
        name = "<unknown>"

    return _do_fmt_placeholder_node(name, l2tobj)


def fmt_matrix_environment_node(node, l2tobj):
    r"""
    This function can be used as a callable in :py:class:`EnvironmentTextSpec`
    for matrix-like environments like ``\begin{bmatrix}...\end{bmatrix}``.

    The contents is parsed by separating columns with ``&``'s and rows with
    ``\\``'s, and is rendered in the form ``[  a11  a12  ;  a21  a22  ]``.

    .. versionadded:: 2.8

       This function was introduced in `pylatexenc 2.8`.
    """

    class StateType:
        def __init__(self):
            self.matrix_rows = []
            self.buffer_this_column = []
            self.buffer_nodes = []

        def add_content(self, node):
            self.buffer_nodes.append(node)

        def new_column(self):
            if self.buffer_nodes:
                self.buffer_this_column.append(
                    l2tobj.nodelist_to_text(self.buffer_nodes).strip()
                )
            self.buffer_nodes = []

        def new_row(self):
            self.new_column()
            self.matrix_rows.append(self.buffer_this_column)
            self.buffer_this_column = []

    state = StateType()

    # iterate the nodelist and find column and row separators
    for n in node.nodelist:
        if n.isNodeType(latexwalker.LatexSpecialsNode) and n.specials_chars == "&":
            # column separator
            state.new_column()
            continue
        if n.isNodeType(latexwalker.LatexMacroNode) and n.macroname == "\\":
            # row separator
            state.new_row()
            continue
        state.add_content(n)

    state.new_row()  # finish the last row

    # now format the contents as array --
    max_char_width = max((len(x) for row in state.matrix_rows for x in row))
    matrix_contents = "; ".join(
        (
            " ".join((x.rjust(max_char_width, " ") for x in row))
            for row in state.matrix_rows
        )
    )
    return "[ " + matrix_contents + " ]"


#
# see reference: https://unicode.org/charts/PDF/U1D400.pdf
#

_fmt_math_style_offsets = {
    "bold": (0x1D400, 0x1D41A),
    "italic": (0x1D434, 0x1D44E),
    "bold-italic": (0x1D468, 0x1D482),
    "script": (0x1D49C, 0x1D4B6),
    "bold-script": (0x1D4D0, 0x1D4EA),
    "fraktur": (0x1D504, 0x1D51E),
    "doublestruck": (0x1D538, 0x1D552),
    "bold-fraktur": (0x1D56C, 0x1D586),
    "sans": (0x1D5A0, 0x1D5BA),
    "sans-bold": (0x1D5D4, 0x1D5EE),
    "sans-italic": (0x1D608, 0x1D622),
    "sans-bold-italic": (0x1D63C, 0x1D656),
    "monospace": (0x1D670, 0x1D68A),
}
# account for "holes" in code point chart because some symbols have already
# been allocated earlier code points (see reference linked above)
_fmt_math_style_exceptions = {
    "italic": {
        ord("h"): chr(0x210E),  # PLANK CONSTANT
    },
    "script": {
        ord("B"): chr(0x212C),
        ord("E"): chr(0x2130),
        ord("F"): chr(0x2131),
        ord("H"): chr(0x210B),
        ord("I"): chr(0x2110),
        ord("L"): chr(0x2112),
        ord("M"): chr(0x2133),
        ord("R"): chr(0x211B),
        ord("e"): chr(0x212F),
        ord("g"): chr(0x210A),
        ord("o"): chr(0x2134),
    },
    "fraktur": {
        ord("C"): chr(0x212D),
        ord("H"): chr(0x210C),
        ord("I"): chr(0x2111),
        ord("R"): chr(0x211C),
        ord("Z"): chr(0x2128),
    },
    "doublestruck": {
        ord("C"): chr(0x2102),
        ord("H"): chr(0x210D),
        ord("N"): chr(0x2115),
        ord("P"): chr(0x2119),
        ord("Q"): chr(0x211A),
        ord("R"): chr(0x211D),
        ord("Z"): chr(0x2124),
    },
}

_oA, _oZ, _oa, _oz = ord("A"), ord("Z"), ord("a"), ord("z")


def _fmt_math_style_char(c, style):
    oc = ord(c)
    z = _fmt_math_style_exceptions.get(style, {}).get(oc, None)
    if z is not None:
        return z

    offset_up, offset_lo = _fmt_math_style_offsets.get(
        style,
        (
            _oA,
            _oa,
        ),
    )

    if oc >= _oA and oc <= _oZ:
        return chr(offset_up + oc - _oA)
    if oc >= _oa and oc <= _oz:
        return chr(offset_lo + oc - _oa)

    # don't know how to handle this char
    return c


if sys.maxunicode < 0x10FFFF:
    # narrow python build, disable math alphabets.
    _fmt_math_style_char = lambda c, style: c


def fmt_math_text_style(text, style):
    r"""
    Return the text with letters replaced by unicode characters so that the
    style `style` is applied.  (We use the unicode math alphanumeric symbols,
    see `https://unicode.org/charts/PDF/U1D400.pdf`_.)

    The `style` must be one of 'bold', 'italic', 'bold-italic', 'script',
    'bold-script', 'fraktur', 'doublestruck', 'bold-fraktur', 'sans',
    'sans-bold', 'sans-italic', 'sans-bold-italic', or 'monospace'.

    The character `c` is essentially expected to be an ascii letter, and any
    other character will be returned unchanged.  (Possible exceptions might be
    implemented in the future, for instance to implement the double-struck
    one/identity operator ``\mathbbm{1}``.)
    """
    return "".join((_fmt_math_style_char(c, style=style) for c in text))


def get_default_latex_context_db():
    r"""
    Return a :py:class:`pylatexenc.macrospec.LatexContextDb` instance
    initialized with a collection of text replacements for known macros and
    environments.

    TODO: clean up and document categories.

    If you want to add your own definitions, you should use the
    :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()`
    method.  If you would like to override some definitions, use that method
    with the argument `prepend=True`.  See docs for
    :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()`.

    If there are too many macro/environment definitions, or if there are some
    irrelevant ones, you can always filter the returned database using
    :py:meth:`pylatexenc.macrospec.LatexContextDb.filter_context()`.

    .. versionadded:: 2.0

       The :py:class:`pylatexenc.macrospec.LatexContextDb` class as well as this
       method, were all introduced in `pylatexenc 2.0`.
    """
    db = macrospec.LatexContextDb()

    from ._defaultspecs import specs

    for cat, catspecs in specs:
        db.add_context_category(
            cat,
            macros=catspecs["macros"],
            environments=catspecs["environments"],
            specials=catspecs["specials"],
        )

    return db


default_macro_dict = _util.LazyDict(
    generate_dict_fn=lambda: dict(
        [(m.macroname, m) for m in get_default_latex_context_db().iter_macro_specs()]
    )
)
r"""
.. deprecated:: 2.0

   Use :py:func:`get_default_latex_context_db()` instead, or create your own
   :py:class:`pylatexenc.macrospec.LatexContextDb` object.


Provide an access to the default macro text replacement specs for `latex2text`
in a form that is compatible with `pylatexenc 1.x`\ 's `default_macro_dict`
module-level dictionary.

This is implemented using a custom lazy mutable mapping, which behaves just like
a regular dictionary but that loads the data only once the dictionary is
accessed.  In this way the default latex specs into a python dictionary unless
they are actually queried or modified, and thus users of `pylatexenc 2.0` that
don't rely on the default macro/environment definitions shouldn't notice any
decrease in performance.
"""

default_env_dict = _util.LazyDict(
    generate_dict_fn=lambda: dict(
        [
            (m.environmentname, m)
            for m in get_default_latex_context_db().iter_environment_specs()
        ]
    )
)
r"""
.. deprecated:: 2.0

   Use :py:func:`get_default_latex_context_db()` instead, or create your own
   :py:class:`pylatexenc.macrospec.LatexContextDb` object.


Provide an access to the default environment text replacement specs for
`latex2text` in a form that is compatible with `pylatexenc 1.x`\ 's
`default_macro_dict` module-level dictionary.

This is implemented using a custom lazy mutable mapping, which behaves just like
a regular dictionary but that loads the data only once the dictionary is
accessed.  In this way the default latex specs into a python dictionary unless
they are actually queried or modified, and thus users of `pylatexenc 2.0` that
don't rely on the default macro/environment definitions shouldn't notice any
decrease in performance.
"""


default_text_replacements = ()
r"""
.. deprecated:: 2.0

   Text replacements are deprecated since `pylatexenc 2.0` with the advent of
   "latex specials".  See :py:meth:`LatexNodes2Text.apply_text_replacements()`
   for a quick solution to keep existing code working if it uses custom text
   replacements.
"""


# ------------------------------------------------------------------------------

_strict_latex_spaces_predef = {
    "based-on-source": {
        "between-macro-and-chars": False,
        "between-latex-constructs": False,
        "after-comment": False,
        "in-equations": None,
    },
    "macros": {
        "between-macro-and-chars": True,
        "between-latex-constructs": True,
        "after-comment": False,
        "in-equations": "based-on-source",
    },
    "except-in-equations": {
        "between-macro-and-chars": True,
        "between-latex-constructs": True,
        "after-comment": True,
        "in-equations": "based-on-source",
    },
}


def _parse_strict_latex_spaces_dict(strict_latex_spaces):
    d = {
        "between-macro-and-chars": False,
        "between-latex-constructs": False,
        "after-comment": False,
        "in-equations": None,
    }
    if strict_latex_spaces is None:
        return d
    elif strict_latex_spaces is False:
        # "False" == the actual default for non-strict latex spaces == "macros"
        return _strict_latex_spaces_predef["macros"]
    elif strict_latex_spaces is True:
        return dict([(k, True) for k in d.keys()])
    elif isinstance(strict_latex_spaces, dict):
        d.update(strict_latex_spaces)
        return d
    elif isinstance(strict_latex_spaces, basestring):
        if strict_latex_spaces == "on":
            return _parse_strict_latex_spaces_dict(True)
        if strict_latex_spaces == "off":
            return _parse_strict_latex_spaces_dict(False)
        if strict_latex_spaces not in _strict_latex_spaces_predef:
            raise ValueError(
                "invalid value for strict_latex_spaces preset: {}".format(
                    strict_latex_spaces
                )
            )

        if strict_latex_spaces == "default":  # deprecated -- report this
            # compatibility with pylatexenc 1.x, but it is no longer the default!!
            _util.pylatexenc_deprecated_2(
                "The value 'default' for `strict_latex_spaces=` in LatexNodes2Text() "
                "is deprecated. The actual default changed to 'macros', and for "
                "backwards compatibility the obsolete value 'default' still refers to "
                "the earlier default which is now called 'based-on-source'.",
                stacklevel=4,
            )
            strict_latex_spaces = "based-on-source"

        return _strict_latex_spaces_predef[strict_latex_spaces]
    else:
        raise ValueError(
            "Invalid value for strict_latex_spaces: {!r}".format(strict_latex_spaces)
        )


class LatexNodes2Text(object):
    r"""
    Simplistic Latex-To-Text Converter.

    This class parses a nodes structure generated by the :py:mod:`latexwalker` module,
    and creates a text representation of the structure.

    It is capable of parsing ``\input`` directives safely, see
    :py:meth:`set_tex_input_directory()` and :py:meth:`read_input_file()`.  By default,
    ``\input`` and ``\include`` directives are ignored.

    Arguments to the constructor:

    - `latex_context_db` is a :py:class:`pylatexenc.macrospec.LatexContextDb`
      class storing a collection of rules for converting macros, environments,
      and other latex specials to text.  The `LatexContextDb` should contain
      specifications via :py:class:`MacroTextSpec`,
      :py:class:`EnvironmentTextSpec`, and :py:class:`SpecialsTextSpec` objects.

      The default latex context database can be obtained using
      :py:func:`get_default_latex_context_db()`.

    Additional keyword arguments are flags which may influence the behavior:

    - `math_mode='text'|'with-delimiters'|'verbatim'|'remove'`: Specify how to
      treat chunks of LaTeX code that correspond to math modes.  If 'text' (the
      default), then the math mode contents is incorporated as normal text.  If
      'with-delimiters', the content is incorporated as normal text but it is
      still included in the original math-mode delimiters, such as '$...$'.  If
      'verbatim', then the math mode chunk is kept verbatim, including the
      delimiters.  The value 'remove' means to remove the math mode sections
      entirely and not to produce any replacement text.

    - `keep_comments=True|False`: If set to `True`, then LaTeX comments are kept
      (including the percent-sign); otherwise they are discarded.  (By default
      this is `False`)

    - `fill_text`: If set to `True` or to a positive integer, then the
      whitespace of LaTeX char blocks is re-layed out to fill at the given
      number of characters or 80 by default.  The fill is by far not perfect,
      but the resulting text might be slightly more readable.

    - `strict_latex_spaces=True|False`: If set to `True`, then we follow closely
      LaTeX's handling of whitespace.  For instance, whitespace following a bare
      macro (i.e. without any delimiting characters like '{') is
      consumed/removed.  If set to `False` (the default), then some liberties
      are taken with respect to whitespace [hopefully making the result slightly
      more aesthetic, but this behavior is mostly there for historical reasons].

      You may also use one of the presets
      `strict_latex_spaces='based-on-source'|'macros'|'except-in-equations'`,
      which allow for finer control of how whitespace is handled:

        - The value 'based-on-source' is the option that is furthest from
          latex's behavior with spaces, and takes liberties in incuding spaces
          that are present in the source file in several situations where LaTeX
          would remove them, including after macros.  This is meant to be
          hopefully slightly more aesthetic.  However, this option might
          inadvertently break up words: For instance::

              Sk\l odowska

          would be replaced by::

             Skł odowska

        - The value 'macros' is the same as specifying
          `strict_latex_spaces=False`, and it is the default.  It will make
          macros and other sequences of LaTeX constructions obey LaTeX space
          rules, but will keep indentations after comments and keep more liberal
          whitespace rules in equations for a hopefully more aesthetic result.

        - The 'except-in-equations' preset goes as you would expect, setting
          strict latex spacing only outside of equation contexts.

      Finally, the argument `strict_latex_spaces` may also be set to a
      dictionary with keys 'between-macro-and-chars', 'after-comment',
      'between-latex-constructs', and 'in-equations', with individual values
      either `True` or `False`, dictating whitespace behavior in specific cases
      (`True` indicates strict latex behavior).  The value for 'in-equations'
      may even be another dictionary with the same keys to override values in
      equations.  A value of `False` for 'in-equation' has the same meaning as
      'macros'.

      .. versionchanged:: 2.0

         Since `pylatexenc 2.0`, the default value of `strict_latex_spaces` is
         'macros', and no longer 'based-on-source'.

      .. deprecated:: 2.0

         The value 'default' is also accepted, but it is no longer the default!
         It is an alias for 'based-on-source'

      .. versionchanged:: 2.6

         In `pylatexenc` versions 2.0–2.5, contrary to the documentation, the
         default value of `strict_latex_spaces` was actually still
         'based-on-source'.  This bug was fixed in version 2.6, so that now, the
         default setting is actually 'macros'.

    - `keep_braced_groups=True|False`: If set to `True`, then braces delimiting
      a TeX group ``{Like this}`` will be kept in the output, with the contents
      of the group converted to text as usual.  (By default this is `False`)

    - `keep_braced_groups_minlen=<int>`: If `keep_braced_groups` is set to
      `True`, then we keep braced groups only if their contents length (after
      conversion to text) is longer than the given value.  E.g., if
      `keep_braced_groups_minlen=2`, then ``{\'e}tonnant`` still goes to
      ``étonnant`` but ``{\'etonnant}``
      becomes ``{étonnant}``.

    .. versionadded: 1.4

       Added the `strict_latex_spaces`, `keep_braced_groups`, and
       `keep_braced_groups_minlen` flags

    .. versionadded: 2.0

       Added the `math_mode=` flag to replace the poorly designed
       `keep_inline_math=` flag;

       Added the `fill_text=` flag.

    Additionally, the following arguments are accepted for backwards compatibility:

    - `keep_inline_math=True|False`: Obsolete since `pylatexenc 2`.  If set to
      `True`, then this is the same as `math_mode='verbatim'`, and if set to
      `False`, this is the same as `math_mode='text'`.

      .. deprecated:: 2.0

         The `keep_inline_math=` option is deprecated because it had a weird
         behavior and was poorly implemented, especially given that a similarly
         named option in :py:class:`LatexWalker` had a different effect.  See
         issue :issue:`14`.

    - `text_replacements` this argument is ignored starting from `pylatexenc 2`.

      .. deprecated:: 2.0

         Text replacements are no longer made at the end of the text conversion.
         This feature is replaced by the concept of LaTeX specials---see, e.g.,
         :py:class:`pylatexenc.latexwalker.LatexSpecialsNode`.

         To keep existing code working, add a call to
         :py:meth:`apply_text_replacements()` immediately after
         :py:meth:`nodelist_to_text()` to achieve the same effect as in
         `pylatexenc 1.x`.  See :py:meth:`apply_text_replacements()`.

    - `env_dict`, `macro_dict`: Obsolete since `pylatexenc 2`.  If set, they are
      dictionaries of known environment and macro definitions.  They default to
      :py:data:`default_env_dict` and :py:data:`default_macro_dict`,
      respectively.

      .. deprecated:: 2.0

         You should now use the more powerful option `latex_context_db=`.  You
         cannot specify both `macro_list` (or `env_list`) and
         `latex_context_db`.
    """

    def __init__(self, latex_context=None, **flags):
        super(LatexNodes2Text, self).__init__()

        if latex_context is None:
            if "macro_dict" in flags or "env_dict" in flags:
                # LEGACY -- build a latex context using the given macro_dict
                _util.pylatexenc_deprecated_2(
                    "The `macro_dict=...` and `env_dict=...` options in LatexNodes2Text() are "
                    "obsolete since pylatexenc 2.  They will still work, but please consider "
                    "using instead the more versatile option `latex_context=...`."
                )

                macro_dict = flags.pop("macro_dict", [])
                env_dict = flags.pop("env_dict", [])

                latex_context = macrospec.LatexContextDb()
                latex_context.add_context_category(
                    "custom",
                    macros=macro_dict.values(),
                    environments=env_dict.values(),
                    specials=[],
                )

            else:
                # default -- use default
                latex_context = get_default_latex_context_db()

        self.latex_context = latex_context

        self.tex_input_directory = None
        self.strict_input = True

        if "keep_inline_math" in flags:
            if "math_mode" in flags:
                raise TypeError(
                    "Cannot specify both math_mode= and keep_inline_math= "
                    "for LatexNodes2Text()"
                )
            _util.pylatexenc_deprecated_2(
                "The keep_inline_math=... option in LatexNodes2Text() has been replaced by "
                "the math_mode=... option."
            )
            self.math_mode = "verbatim" if flags.pop("keep_inline_math") else "text"
        else:
            self.math_mode = flags.pop("math_mode", "text")

        if self.math_mode not in ("text", "with-delimiters", "verbatim", "remove"):
            raise ValueError(
                "math_mode= option must be one of 'text', 'with-delimiters', "
                "'verbatim', 'remove'"
            )

        self.keep_comments = flags.pop("keep_comments", False)

        strict_latex_spaces = flags.pop("strict_latex_spaces", False)
        self.strict_latex_spaces = _parse_strict_latex_spaces_dict(strict_latex_spaces)

        self.keep_braced_groups = flags.pop("keep_braced_groups", False)
        self.keep_braced_groups_minlen = flags.pop("keep_braced_groups_minlen", 2)

        self.fill_text = flags.pop("fill_text", None)
        if not self.fill_text:  # None, 0, False, or false-ish
            self.fill_text = None
        if self.fill_text is True:  # exactly boolean true, not an int
            self.fill_text = 80

        if "text_replacements" in flags:
            del flags["text_replacements"]
            _util.pylatexenc_deprecated_2(
                "The text_replacements= argument is ignored since pylatexenc 2. "
                "To keep existing code working, add a call to "
                "`LatexNodes2Text.apply_text_replacements()`. "
                'New code should use "latex specials" instead.'
            )

        if flags:
            # any flags left which we haven't recognized
            logger.warning(
                "LatexNodes2Text(): Unknown flag(s) encountered: %r", list(flags.keys())
            )

    def set_tex_input_directory(
        self, tex_input_directory, latex_walker_init_args=None, strict_input=True
    ):
        """
        Set where to look for input files when encountering the ``\\input`` or
        ``\\include`` macro.

        Alternatively, you may also override :py:meth:`read_input_file()` to
        implement a custom file lookup mechanism.

        The argument `tex_input_directory` is the directory relative to which to
        search for input files.

        If `strict_input` is set to `True`, then we always check that the
        referenced file lies within the subtree of `tex_input_directory`,
        prohibiting for instance hacks with '..' in filenames or using symbolic
        links to refer to files out of the directory tree.

        The argument `latex_walker_init_args` allows you to specify the parse
        flags passed to the constructor of
        :py:class:`pylatexenc.latexwalker.LatexWalker` when parsing the input
        file.
        """
        self.tex_input_directory = tex_input_directory
        self.latex_walker_init_args = (
            latex_walker_init_args if latex_walker_init_args else {}
        )
        self.strict_input = strict_input

    def read_input_file(self, fn):
        """
        This method may be overridden to implement a custom lookup mechanism when
        encountering ``\\input`` or ``\\include`` directives.

        The default implementation looks for a file of the given name relative
        to the directory set by :py:meth:`set_tex_input_directory()`.  If
        `strict_input=True` was set, we ensure strictly that the file resides in
        a subtree of the reference input directory (after canonicalizing the
        paths and resolving all symlinks).

        If `set_tex_input_directory()` was not called, or if it was called with
        a value of `None`, then no file system access is attempted an an empty
        string is returned.

        You may override this method to obtain the input data in however way you
        see fit.  In that case, a call to `set_tex_input_directory()` may not be
        needed as that function simply sets properties which are used by the
        default implementation of `read_input_file()`.

        This function accepts the referred filename as argument (the argument to
        the ``\\input`` macro), and should return a string with the file
        contents (or generate a warning or raise an error).
        """

        if self.tex_input_directory is None:
            return ""

        fnfull = os.path.realpath(os.path.join(self.tex_input_directory, fn))
        if self.strict_input:
            # make sure that the input file is strictly within dirfull, and
            # didn't escape with '../..' tricks or via symlinks.
            dirfull = os.path.realpath(self.tex_input_directory)
            if not fnfull.startswith(dirfull):
                logger.warning(
                    "Can't access path '%s' leading outside of mandated directory "
                    "[strict input mode]",
                    fn,
                )
                return ""

        if not os.path.exists(fnfull) and os.path.exists(fnfull + ".tex"):
            fnfull = fnfull + ".tex"
        if not os.path.exists(fnfull) and os.path.exists(fnfull + ".latex"):
            fnfull = fnfull + ".latex"
        if not os.path.isfile(fnfull):
            logger.warning("Error, file doesn't exist: '%s'", fn)
            return ""

        logger.debug("Reading input file %r", fnfull)

        try:
            with open(fnfull) as f:
                return f.read()
        except IOError as e:
            logger.warning("Error, can't access '%s': %s", fn, e)
            return ""

    def _input_node_simplify_repl(self, n):
        #
        # recurse into files upon '\input{}'
        #

        if len(n.nodeargs) != 1:
            logger.warning(
                "Expected exactly one argument for '\\input' ! Got = %r", n.nodeargs
            )

        inputtex = self.read_input_file(self.nodelist_to_text([n.nodeargs[0]]).strip())

        if not inputtex:
            return ""

        return self.nodelist_to_text(
            latexwalker.LatexWalker(
                inputtex, **self.latex_walker_init_args
            ).get_latex_nodes()[0]
        )

    def latex_to_text(self, latex, **parse_flags):
        """
        Parses the given `latex` code and returns its textual representation.

        This is equivalent to constructing a
        :py:class:`pylatexenc.latexwalker.LatexWalker` with the given `latex`
        string, calling its method
        :py:meth:`~pylatexenc.latexwalker.LatexWalker.get_latex_nodes()`, and
        providing the outcome to :py:meth:`nodelist_to_text()`.

        The `parse_flags` are keyword arguments to provide to the
        :py:class:`pylatexenc.latexwalker.LatexWalker` constructor.
        """
        return self.nodelist_to_text(
            latexwalker.LatexWalker(latex, **parse_flags).get_latex_nodes()[0]
        )

    def nodelist_to_text(self, nodelist):
        """
        Extracts text from a node list. `nodelist` is a list of `latexwalker` nodes,
        typically returned by
        :py:meth:`pylatexenc.latexwalker.LatexWalker.get_latex_nodes()`.

        This function basically applies `node_to_text()` to each node and
        concatenates the results into one string.  (This is not quite actually
        the case, since we take some care as to where we add whitespace
        according to the class options.)
        """

        s = ""
        prev_node = None
        for node in nodelist:
            if self._is_bare_macro_node(prev_node) and node.isNodeType(
                latexwalker.LatexCharsNode
            ):

                if not self.strict_latex_spaces["between-macro-and-chars"]:
                    # after a macro with absolutely no arguments, include
                    # post_space in output by default if there are other chars
                    # that follow.  This is for more breathing space (especially
                    # in equations(?)), and for compatibility with earlier
                    # versions of pylatexenc (<= 1.3).  This is NOT LaTeX'
                    # default behavior (see issue #11), so only do this if the
                    # corresponding `strict_latex_spaces=` flag is set.
                    s += prev_node.macro_post_space

            last_nl_pos = s.rfind("\n")
            if last_nl_pos != -1:
                textcol = len(s) - last_nl_pos - 1
            else:
                textcol = len(s)

            s += self.node_to_text(node, textcol=textcol)

            prev_node = node

        return s

    def node_to_text(self, node, prev_node_hint=None, textcol=0):
        """
        Return the textual representation of the given `node`.

        If `prev_node_hint` is specified, then the current node is formatted
        suitably as following the node given in `prev_node_hint`.  This might
        affect how much space we keep/discard, etc.
        """
        if node is None:
            return ""

        # ### It doesn't look like we use prev_node_hint at all.  Eliminate at
        # ### some point?

        if node.isNodeType(latexwalker.LatexCharsNode):
            return self.chars_node_to_text(node, textcol=textcol)

        if node.isNodeType(latexwalker.LatexCommentNode):
            return self.comment_node_to_text(node)

        if node.isNodeType(latexwalker.LatexGroupNode):
            return self.group_node_to_text(node)

        if node.isNodeType(latexwalker.LatexMacroNode):
            return self.macro_node_to_text(node)

        if node.isNodeType(latexwalker.LatexEnvironmentNode):
            return self.environment_node_to_text(node)

        if node.isNodeType(latexwalker.LatexSpecialsNode):
            return self.specials_node_to_text(node)

        if node.isNodeType(latexwalker.LatexMathNode):
            return self.math_node_to_text(node)

        logger.warning("LatexNodes2Text.node_to_text(): Unknown node: %r", node)

        # discard anything else.
        return ""

    def chars_node_to_text(self, node, textcol=0):
        r"""
        Return the textual representation of the given `node` representing a block
        of simple latex text with no special characters or macros.  The `node`
        is :py:class:`~pylatexenc.latexwalker.LatexCharsNode`.
        """
        # Unless in strict latex spaces mode, ignore nodes consisting only
        # of empty chars, as this tends to produce too much space...  These
        # have been inserted by LatexWalker() in some occasions to keep
        # track of all relevant pre_space of tokens, such as between two
        # braced groups ("{one} {two}") or other such situations.
        content = node.chars
        if self.fill_text:  # None or column width
            content = self.do_fill_text(content, textcol=textcol)
        if (
            not self.strict_latex_spaces["between-latex-constructs"]
            and len(content.strip()) == 0
        ):
            return ""
        return content

    def comment_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing a latex
        comment.  The `node` is
        :py:class:`~pylatexenc.latexwalker.LatexCommentNode`.
        """
        if self.keep_comments:
            if self.strict_latex_spaces["after-comment"]:
                nl = "\n"
                if node.comment_post_space == "":
                    # this happens if two newlines follow a comment---the
                    # comment_post_space is empty, and the \n\n is reported as a
                    # char node to notify that there is a new paragraph.
                    nl = ""
                return "%" + node.comment + nl
            else:
                # default spaces, i.e., keep what spaces were already there
                # after the comment
                return "%" + node.comment + node.comment_post_space
        else:
            if self.strict_latex_spaces["after-comment"]:
                return ""
            else:
                # default spaces, i.e., keep what spaces were already there
                # after the comment.  This can be useful to preserve
                # e.g. indentation of the next line
                return node.comment_post_space

    def group_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing a latex
        group.  The `node` is
        :py:class:`~pylatexenc.latexwalker.LatexGroupNode`.
        """
        contents = self._groupnodecontents_to_text(node)
        if self.keep_braced_groups and len(contents) >= self.keep_braced_groups_minlen:
            return node.delimiters[0] + contents + node.delimiters[1]
        return contents

    def macro_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing a latex
        macro invocation.  The `node` is
        :py:class:`~pylatexenc.latexwalker.LatexMacroNode`.
        """
        # get macro behavior definition.
        macroname = node.macroname
        mac = self.latex_context.get_macro_spec(macroname)
        if mac is None:
            # default for unknown macros
            mac = MacroTextSpec("", discard=True)

        def get_macro_str_repl(node, macroname, mac):
            if mac.simplify_repl:
                return self.apply_simplify_repl(
                    node, mac.simplify_repl, what=r"macro '\%s'" % (macroname)
                )
            if mac.discard:
                return ""
            a = []
            if node.nodeargd and node.nodeargd.argnlist:
                a = node.nodeargd.argnlist
            return "".join([self._groupnodecontents_to_text(n) for n in a])

        macrostr = get_macro_str_repl(node, macroname, mac)
        return macrostr

    def environment_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing a full
        latex environment.  The `node` is
        :py:class:`~pylatexenc.latexwalker.LatexEnvironmentNode`.
        """
        # get environment behavior definition.
        environmentname = node.environmentname
        envdef = self.latex_context.get_environment_spec(environmentname)
        if envdef is None:
            # default for unknown environments
            envdef = EnvironmentTextSpec("", discard=False)

        if envdef.simplify_repl:
            return self.apply_simplify_repl(
                node, envdef.simplify_repl, what="environment '%s'" % (environmentname)
            )
        if envdef.discard:
            return ""

        return self.nodelist_to_text(node.nodelist)

    def specials_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing special a
        latex character (or characters).  The `node` is
        :py:class:`~pylatexenc.latexwalker.LatexSpecialsNode`.
        """
        # get the specials text spec
        specials_chars = node.specials_chars
        sspec = self.latex_context.get_specials_spec(specials_chars)
        if sspec is None:
            # no corresponding spec, leave the special chars unchanged:
            return specials_chars

        def get_specials_str_repl(node, specials_chars, spec):
            if spec.simplify_repl:
                return self.apply_simplify_repl(
                    node, spec.simplify_repl, what="specials '%s'" % (specials_chars)
                )
            if spec.discard:
                return ""
            if node.nodeargd and node.nodeargd.argnlist:
                a = node.nodeargd.argnlist
            return "".join([self._groupnodecontents_to_text(n) for n in a])

        s = get_specials_str_repl(node, specials_chars, sspec)
        return s

    def math_node_to_text(self, node):
        r"""
        Return the textual representation of the given `node` representing a block
        of math mode latex.  The `node` is either a
        :py:class:`~pylatexenc.latexwalker.LatexMathNode` or a
        :py:class:`~pylatexenc.latexwalker.LatexEnvironmentNode`.

        This method is responsible for honoring the `math_mode=...` option
        provided to the constructor.
        """

        if self.math_mode == "verbatim":
            if (
                node.isNodeType(latexwalker.LatexEnvironmentNode)
                or node.displaytype == "display"
            ):
                return self._fmt_indented_block(node.latex_verbatim(), indent="")
            else:
                return node.latex_verbatim()

        elif self.math_mode == "remove":
            return ""

        elif self.math_mode == "with-delimiters":
            with _PushEquationContext(self):
                content = self.nodelist_to_text(node.nodelist).strip()
            if node.isNodeType(latexwalker.LatexMathNode):
                delims = node.delimiters
            else:  # environment node
                delims = (
                    r"\begin{%s}" % (node.environmentname),
                    r"\end{%s}" % (node.environmentname),
                )
            if (
                node.isNodeType(latexwalker.LatexEnvironmentNode)
                or node.displaytype == "display"
            ):
                return (
                    delims[0] + self._fmt_indented_block(content, indent="") + delims[1]
                )
            else:
                return delims[0] + content + delims[1]

        elif self.math_mode == "text":
            with _PushEquationContext(self):
                content = self.nodelist_to_text(node.nodelist).strip()
            if (
                node.isNodeType(latexwalker.LatexEnvironmentNode)
                or node.displaytype == "display"
            ):
                return self._fmt_indented_block(content)
            else:
                return content

        else:
            raise RuntimeError("unknown math_mode={} !".format(self.math_mode))

    def do_fill_text(self, text, textcol=0):
        # keep trailing whitespace to have whitespace between macros in text as
        # in "see \ref{...} and blah blah"
        head_ws = re.search(r"^\s*", text).group()
        head_par = "\n\n" if ("\n\n" in head_ws) else ""
        # head_nl = '\n' if (not head_par and '\n' in head_ws) else ''
        trail_ws = re.search(r"\s*$", text).group()
        trail_par = "\n\n" if ("\n\n" in trail_ws) else ""
        # trail_nl = '\n' if (not trail_par and '\n' in trail_ws) else ''
        text = text.strip()

        def fill_chunk(x, textcol):
            # head_ws = ' ' if textcol>0 and x[0:1].isspace() else ''
            # trail_ws = ' ' if x[-1:].isspace() else ''
            head_ws, trail_ws = "", ""
            x = x.strip()
            if textcol >= self.fill_text - 4:
                return "\n" + textwrap.fill(x, self.fill_text) + trail_ws
            else:
                return (
                    head_ws
                    + textwrap.fill(x, self.fill_text, initial_indent="X" * textcol)[
                        textcol:
                    ]
                    + trail_ws
                )

        rawchunks = re.compile(r"\n{2,}").split(text)

        chunks = [
            thechunk
            for (j, thechunk) in (
                (j, fill_chunk(x, textcol if j == 0 else 0))
                for j, x in enumerate(rawchunks)
            )
            if thechunk.strip()
        ]

        return (
            head_par
            + (" " if textcol > 0 and head_ws and not head_par else "")
            + "\n\n".join(chunks)
            + (" " if trail_ws and not trail_par else "")
            + trail_par
        )

    def apply_simplify_repl(self, node, simplify_repl, what):
        r"""
        Utility to get the replacement text associated with a `node` for which we
        have a `simplify_repl` object (given by e.g. a MacroTextSpec or
        similar).

        The argument `what` is used in error messages.
        """
        if callable(simplify_repl):
            kwargs = {}
            fn_args = getfullargspec(simplify_repl)[0]
            if "l2tobj" in fn_args:
                # callable accepts an argument named 'l2tobj', provide pointer to self
                kwargs["l2tobj"] = self
            if (
                node.isNodeType(latexwalker.LatexEnvironmentNode)
                and "environmentname" in fn_args
            ):
                kwargs["environmentname"] = node.environmentname
            if node.isNodeType(latexwalker.LatexMacroNode) and "macroname" in fn_args:
                kwargs["macroname"] = node.macroname
            if (
                node.isNodeType(latexwalker.LatexSpecialsNode)
                and "specials_chars" in fn_args
            ):
                kwargs["specials_chars"] = node.specials_chars

            r = simplify_repl(node, **kwargs)
            if r:
                return r
            return ""  # don't return None

        if "%" in simplify_repl and len(simplify_repl) != 1:
            # if simplify_repl contains a '%' sign then we will look for %-based
            # formatting placeholder(s), except if simplify_repl is the string
            # '%' itself (checked above with "len(simplify_repl)!=1") in which
            # case it is a literal replacement percent symbol.

            nodeargs = []
            if node.nodeargd and node.nodeargd.argnlist:
                nodeargs = node.nodeargd.argnlist

            has_percent_s = re.search("(^|[^%])(%%)*%s", simplify_repl)

            if node.isNodeType(latexwalker.LatexEnvironmentNode):
                if has_percent_s:
                    x = (self.nodelist_to_text(node.nodelist),)
                else:
                    x = dict(
                        (str(1 + j), val)
                        for j, val in enumerate(
                            self._groupnodecontents_to_text(nn) for nn in nodeargs
                        )
                    )
                    x.update(body=self.nodelist_to_text(node.nodelist))
            elif has_percent_s:
                x = tuple([self._groupnodecontents_to_text(nn) for nn in nodeargs])
            else:
                x = dict(
                    (str(1 + j), val)
                    for j, val in enumerate(
                        self._groupnodecontents_to_text(nn) for nn in nodeargs
                    )
                )

            try:
                return simplify_repl % x
            except (TypeError, ValueError, KeyError):
                logger.warning(
                    "WARNING: Error in configuration: {} failed its substitution!".format(
                        what
                    )
                )
                return simplify_repl  # too bad, keep the percent signs as they are...
        return simplify_repl

    def _fmt_indented_block(self, contents, indent=" " * 4):
        block = "\n" + indent + contents.replace("\n", "\n" + indent) + "\n"
        if self.fill_text:
            # additional newlines because neighboring text gets trimmed
            block = "\n" + block + "\n"
        return block

    def _is_bare_macro_node(self, node):
        return (
            node is not None
            and node.isNodeType(latexwalker.LatexMacroNode)
            and node.nodeoptarg is None
            and len(node.nodeargs) == 0
        )

    def _groupnodecontents_to_text(self, groupnode):
        if groupnode is None:
            return ""
        if not groupnode.isNodeType(latexwalker.LatexGroupNode):
            return self.node_to_text(groupnode)
        return self.nodelist_to_text(groupnode.nodelist)

    def node_arg_to_text(self, node, k):
        r"""
        Return the textual representation of the `k`\ -th argument of the given
        `node`.  This might be useful for substitution lambdas in macro and
        environment specs.
        """
        if node.nodeargd and node.nodeargd.argnlist:
            return self._groupnodecontents_to_text(node.nodeargd.argnlist[k])
        return ""

    def apply_text_replacements(self, s, text_replacements):
        r"""
        Convenience function for code that used `text_replacements=` in `pylatexenc
        1.x`.

        If you used custom `text_replacements=` in `pylatexenc 1.x` then you
        will have to change::

          # pylatexenc 1.x with text_replacements
          text_replacements = ...
          l2t = LatexNodes2Text(..., text_replacements=text_replacements)
          text = l2t.nodelist_to_text(...)

        to::

          # pylatexenc 2 text_replacements compatibility code
          text_replacements = ...
          l2t = LatexNodes2Text(...)
          temp = l2t.nodelist_to_text(...)
          text = l2t.apply_text_replacements(temp, text_replacements)

        as a quick fix.  It is recommended however to treat text replacements
        instead as "latex specials".  (Otherwise the brutal text replacements
        might act on text generated from macros and environments and give
        unwanted results.)  See :py:class:`pylatexenc.macrospec.SpecialsSpec`
        and :py:class:`SpecialsTextSpec`.

        .. deprecated:: 2.0

           The `apply_text_replacements()` method was introduced in `pylatexenc
           2.0` as a deprecated method.  You can use it as a quick fix to make
           existing code run as it did in `pylatexenc 1.x`.  Its use is however
           not recommended for new code.  You should use "latex specials"
           instead for characters that have special LaTeX meaning.
        """

        # perform suitable replacements
        for pattern, replacement in text_replacements:
            if hasattr(pattern, "sub"):
                s = pattern.sub(replacement, s)
            else:
                s = s.replace(pattern, replacement)

        return s


class _PushEquationContext(latexwalker._PushPropOverride):
    def __init__(self, l2t):

        new_strict_latex_spaces = None
        if l2t.strict_latex_spaces["in-equations"] is not None:
            new_strict_latex_spaces = _parse_strict_latex_spaces_dict(
                l2t.strict_latex_spaces["in-equations"]
            )

        super(_PushEquationContext, self).__init__(
            l2t, "strict_latex_spaces", new_strict_latex_spaces
        )


# ------------------------------------------------------------------------------


def latex2text(
    content, tolerant_parsing=False, keep_inline_math=False, keep_comments=False
):
    """
    Heuristic conversion of LaTeX content `content` to unicode text.

    .. deprecated:: 1.0
       Please use :py:class:`LatexNodes2Text` instead.
    """

    _util.pylatexenc_deprecated_ver(
        "1.0",
        "The module-level function `pylatexenc.latex2text.latex2text()` is deprecated "
        "in favor of the `pylatexenc.latex2text.LatexNodes2Text` class.",
    )

    (nodelist, tpos, tlen) = latexwalker.get_latex_nodes(
        content, keep_inline_math=keep_inline_math, tolerant_parsing=tolerant_parsing
    )

    return latexnodes2text(
        nodelist, keep_inline_math=keep_inline_math, keep_comments=keep_comments
    )


def latexnodes2text(nodelist, keep_inline_math=False, keep_comments=False):
    """
    Extracts text from a node list. `nodelist` is a list of nodes as returned by
    :py:func:`pylatexenc.latexwalker.get_latex_nodes()`.

    .. deprecated:: 1.0
       Please use :py:class:`LatexNodes2Text` instead.
    """

    _util.pylatexenc_deprecated_ver(
        "1.0",
        "The module-level function `pylatexenc.latex2text.latexnodes2text()` is "
        "deprecated in favor of the `pylatexenc.latex2text.LatexNodes2Text` class.",
    )

    return LatexNodes2Text(
        keep_inline_math=keep_inline_math, keep_comments=keep_comments
    ).nodelist_to_text(nodelist)
