great-ai/great_ai/utilities/external/pylatexenc/latex2text/__init__.py
2022-07-04 19:31:15 +02:00

1578 lines
59 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)