great-ai/great_ai/utilities/external/pylatexenc/macrospec/_argparsers.py
Andras Schmelczer 8faee98ec6
Some checks failed
Publish documentation / publish (push) Successful in 58s
Check / Lint, format & type checks (push) Failing after 1m2s
Check / Test on Python 3.10 (push) Successful in 1m9s
Check / Test on Python 3.9 (push) Successful in 50s
Modernise
2026-06-06 21:39:06 +01:00

497 lines
19 KiB
Python

# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2019 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.
#
# Internal module. Internal API may move, disappear or otherwise change at any
# time and without notice.
import sys
if sys.version_info.major > 2:
# Py3
def unicode(s):
return s
_basestring = str
_str_from_unicode = lambda x: x
_unicode_from_str = lambda x: x
else:
# Py2
_basestring = basestring
_str_from_unicode = lambda x: unicode(x).encode("utf-8")
_unicode_from_str = lambda x: x.decode("utf-8")
class ParsedMacroArgs(object):
r"""
Parsed representation of macro arguments.
The base class provides a simple way of storing the arguments as a list of
parsed nodes.
This base class can be subclassed to store additional information and
provide more advanced APIs to access macro arguments for certain categories
of macros.
Arguments:
- `argnlist` is a list of latexwalker nodes that represent macro
arguments. If the macro arguments are too complicated to store in a
list, leave this as `None`. (But then code that uses the latexwalker
must be aware of your own API to access the macro arguments.)
The difference between `argnlist` and the legacy `nodeargs` is that all
options, regardless of optional or mandatory, are stored in the list
`argnlist` with possible `None`\ 's at places where optional arguments
were not provided. Previously, whether a first optional argument was
included in `nodeoptarg` or `nodeargs` depended on how the macro
specification was given.
- `argspec` is a string or a list that describes how each corresponding
argument in `argnlist` represents. If the macro arguments are too
complicated to store in a list, leave this as `None`. For standard
macros and parsed arguments this is a string with characters '*', '[',
'{' describing an optional star argument, an optional
square-bracket-delimited argument, and a mandatory argument.
Attributes:
.. py:attribute:: argnlist
The list of latexwalker nodes that was provided to the constructor
.. py:attribute:: argspec
Argument type specification provided to the constructor
.. py:attribute:: legacy_nodeoptarg_nodeargs
A tuple `(nodeoptarg, nodeargs)` that should be exposed as properties in
:py:class:`~pylatexenc.latexwalker.LatexMacroNode` to provide (as best as
possible) compatibility with pylatexenc < 2.
This is either `(<1st optional arg node>, <list of remaining args>)` if
the first argument is optional and all remaining args are mandatory; or
it is `(None, <list of args>)` for any other argument structure.
"""
def __init__(self, argnlist=[], argspec="", **kwargs):
super(ParsedMacroArgs, self).__init__(**kwargs)
self.argnlist = argnlist
self.argspec = argspec
# for LatexMacroNode to provide some kind of compatibility with pylatexenc < 2
self.legacy_nodeoptarg_nodeargs = self._get_legacy_attribs(
self.argspec, self.argnlist
)
def _get_legacy_attribs(self, argspec, argnlist):
nskip = 0
while argspec.startswith("*"):
argspec = argspec[1:]
nskip += 1
if argspec[0:1] == "[" and all(x == "{" for x in argspec[1:]):
return (argnlist[nskip], argnlist[nskip + 1 :])
else:
return (None, argnlist)
def to_json_object(self):
r"""
Called when we export the node structure to JSON when running latexwalker in
command-line.
Return a representation of the current parsed arguments in an object,
typically a dictionary, that can easily be exported to JSON. The object
may contain latex nodes and other parsed-argument objects, as we use a
custom JSON encoder that understands these types.
Subclasses may
"""
return dict(
argspec=self.argspec,
argnlist=self.argnlist,
)
def __repr__(self):
return "{}(argspec={!r}, argnlist={!r})".format(
self.__class__.__name__, self.argspec, self.argnlist
)
class MacroStandardArgsParser(object):
r"""
Parses the arguments to a LaTeX macro.
This class parses a simple macro argument specification with a specified
arrangement of optional and mandatory arguments.
This class also serves as base class for more advanced argument parsers
(e.g. for a ``\verb+...+`` macro argument parser). In such cases,
subclasses should attempt to provide the most suitable `argspec` (and
`argnlist` for the corresponding :py:class:`ParsedMacroArgs`) for their use,
if appropriate, or set them to `None`.
Arguments:
- `argspec`: must be a string in which each character corresponds to an
argument. The character '{' represents a mandatory argument (single
token or LaTeX group) and the character '[' denotes an optional argument
delimited by braces. The character '\*' denotes a possible star char at
that position in the argument list, a corresponding
``latexwalker.LatexCharsNode('*')`` (or `None` if no star) will be
inserted in the argument node list. For instance, the string '\*{[[{'
would be suitable to specify the signature of the '\\newcommand' macro.
Currently, the argspec string may only contain the characters '\*', '{'
and '['.
The `argspec` may also be `None`, which is the same as specifying an
empty string.
- `optional_arg_no_space`: If set to `True`, then an optional argument
cannot have any whitespace between the preceeding tokens and the '['
character. Set this to `True` in cases such as for ``\\`` in AMS-math
environments, where AMS apparently introduced a patch to prevent a
bracket on a new line after ``\\`` from being interpreted as the
optional argument to ``\\``.
- `args_math_mode`: Either `None`, or a list of the same length as
`argspec`. If a list is given, then each item must be `True`, `False`,
or `None`. The corresponding argument (cf. `argspec`) is then
respectively parsed in math mode (`True`), in text mode (`False`), or
with the mode unchanged (`None`). If `args_math_mode` is `None`, then
all arguments are parsed in the same mode as the current mode.
- additional unrecognized keyword arguments are passed on to superclasses
in case of multiple inheritance
Attributes:
.. py:attribute:: argspec
Argument type specification provided to the constructor.
.. py:attribute:: optional_arg_no_space
See the corresponding constructor argument.
.. py:attribute:: args_math_mode
See the corresponding constructor argument.
"""
def __init__(
self, argspec=None, optional_arg_no_space=False, args_math_mode=None, **kwargs
):
super(MacroStandardArgsParser, self).__init__(**kwargs)
self.argspec = argspec if argspec else ""
self.optional_arg_no_space = optional_arg_no_space
self.args_math_mode = args_math_mode
# catch bugs, make sure that argspec is a string with only accepted chars
if not isinstance(self.argspec, _basestring) or not all(
x in "*[{" for x in self.argspec
):
raise TypeError(
"argspec must be a string containing chars '*', '[', '{{' only: {!r}".format(
self.argspec
)
)
# non-documented attribute that makes us ignore any leading '*'. We use
# this to emulate pylatexenc 1.x behavior when using the MacrosDef()
# function explicitly
self._like_pylatexenc1x_ignore_leading_star = False
def parse_args(self, w, pos, parsing_state=None):
r"""
Parse the arguments encountered at position `pos` in the
:py:class:`~pylatexenc.latexwalker.LatexWalker` instance `w`.
You may override this function to provide custom parsing of complicated
macro arguments (say, ``\verb+...+``). The method will be called by
keyword arguments, so the argument names should not be altered.
The argument `w` is the :py:class:`pylatexenc.latexwalker.LatexWalker`
object that is currently parsing LaTeX code. You can call methods like
`w.get_goken()`, `w.get_latex_expression()` etc., to parse and read
arguments.
The argument `parsing_state` is the current parsing state in the
:py:class:`~pylatexenc.latexwalker.LatexWalker` (e.g., are we currently
in math mode?). See doc for
:py:class:`~pylatexenc.latexwalker.ParsingState`.
This function should return a tuple `(argd, pos, len)` where:
- `argd` is a :py:class:`ParsedMacroArgs` instance, or an instance of a
subclass of :py:class:`ParsedMacroArgs`. The base `parse_args()`
provided here returns a :py:class:`ParsedMacroArgs` instance.
- `pos` is the position of the first parsed content. It should be the
same as the `pos` argument, except if there is whitespace at that
position in which case the returned `pos` would have to be the
position where the argument contents start.
- `len` is the length of the parsed expression. You will probably want
to continue parsing stuff at the index `pos+len` in the string.
"""
from .. import latexwalker
if parsing_state is None:
parsing_state = w.make_parsing_state()
argnlist = []
if self.args_math_mode is not None and len(self.args_math_mode) != len(
self.argspec
):
raise ValueError(
"Invalid args_math_mode={!r} for argspec={!r}!".format(
self.args_math_mode, self.argspec
)
)
def get_inner_parsing_state(j):
if self.args_math_mode is None:
return parsing_state
amm = self.args_math_mode[j]
if amm is None or amm == parsing_state.in_math_mode:
return parsing_state
if amm == True:
return parsing_state.sub_context(in_math_mode=True)
return parsing_state.sub_context(in_math_mode=False)
p = pos
if self._like_pylatexenc1x_ignore_leading_star:
# ignore any leading '*' character
tok = w.get_token(p)
if tok.tok == "char" and tok.arg == "*":
p = tok.pos + tok.len
for j, argt in enumerate(self.argspec):
if argt == "{":
node, np, nl = w.get_latex_expression(
p, strict_braces=False, parsing_state=get_inner_parsing_state(j)
)
p = np + nl
argnlist.append(node)
elif argt == "[":
if self.optional_arg_no_space and p < len(w.s) and w.s[p].isspace():
# don't try to read optional arg, we don't allow space
argnlist.append(None)
continue
optarginfotuple = w.get_latex_maybe_optional_arg(
p, parsing_state=get_inner_parsing_state(j)
)
if optarginfotuple is None:
argnlist.append(None)
continue
node, np, nl = optarginfotuple
p = np + nl
argnlist.append(node)
elif argt == "*":
# possible star.
tok = w.get_token(p)
if tok.tok == "char" and tok.arg.startswith("*"):
# has star
argnlist.append(
w.make_node(
latexwalker.LatexCharsNode,
parsing_state=get_inner_parsing_state(j),
chars="*",
pos=tok.pos,
len=1,
)
)
p = tok.pos + 1
else:
argnlist.append(None)
else:
raise LatexWalkerError(
"Unknown macro argument kind for macro: {!r}".format(argt)
)
parsed = ParsedMacroArgs(
argspec=self.argspec,
argnlist=argnlist,
)
return (parsed, pos, p - pos)
def __repr__(self):
return (
"{}(argspec={!r}, optional_arg_no_space={!r}, args_math_mode={!r})".format(
self.__class__.__name__,
self.argspec,
self.optional_arg_no_space,
self.args_math_mode,
)
)
class ParsedVerbatimArgs(ParsedMacroArgs):
r"""
Parsed representation of arguments to LaTeX verbatim constructs, such as
``\begin{verbatim}...\end{verbatim}`` or ``\verb|...|``.
Instances of `ParsedVerbatimArgs` are returned by the args parser
:py:class:`VerbatimArgsParser`.
Arguments:
- `verbatim_chars_node` --- a properly initialized
:py:class:`pylatexenc.latexwalker.LatexCharsNode` that stores the
verbatim text provided. It is used to initialize the base class
:py:class:`ParsedMacroArgs` to expose a single mandatory argument with
the given verbatim text. The `verbatim_text` attribute is initialized
from this node, too.
- `verbatim_delimiters` --- a 2-item tuple of characters used to delimit
the verbatim arguemnt (in case of a ``\verb+...+`` macro) or `None`.
Attributes:
.. py:attribute:: verbatim_text
The verbatim text that was provided
.. py:attribute:: verbatim_delimiters
If the verbatim text was specified as an argument to ``\verb$...$``, then
this is set to a 2-item tuple that specifies the begin and end
delimiters. Otherwise, the attribute is `None`.
"""
def __init__(self, verbatim_chars_node, verbatim_delimiters=None, **kwargs):
# provide argspec/argnlist to the parent class so that any code that is
# not "verbatim environment-aware" sees this simply as the argument to
# an empty verbatim environment
super(ParsedVerbatimArgs, self).__init__(
argspec="{", argnlist=[verbatim_chars_node], **kwargs
)
self.verbatim_text = verbatim_chars_node.chars
self.verbatim_delimiters = verbatim_delimiters
def __repr__(self):
return "{}(verbatim_text={!r}, verbatim_delimiters={!r})".format(
self.__class__.__name__, self.verbatim_text, self.verbatim_delimiters
)
class VerbatimArgsParser(MacroStandardArgsParser):
r"""
Parses the arguments to various LaTeX "verbatim" constructs such as
``\begin{verbatim}...\end{verbatim}`` environment or ``\verb+...+``.
This class also serves to illustrate how to write custom parsers for
complicated macro arguments. See also :py:class:`MacroStandardArgsParser`.
Arguments:
.. py:attribute:: verbatim_arg_type
One of 'verbatim-environment' or 'verb-macro'.
"""
def __init__(self, verbatim_arg_type, **kwargs):
super(VerbatimArgsParser, self).__init__(argspec="{", **kwargs)
self.verbatim_arg_type = verbatim_arg_type
def parse_args(self, w, pos, parsing_state=None):
from .. import latexwalker
if self.verbatim_arg_type == "verbatim-environment":
# simply scan the string until we find '\end{verbatim}'. That's
# exactly how LaTeX processes it.
endverbpos = w.s.find(r"\end{verbatim}", pos)
if endverbpos == -1:
raise latexwalker.LatexWalkerParseError(
s=w.s, pos=pos, msg=r"Cannot find matching \end{verbatim}"
)
# do NOT include the "\end{verbatim}", latexwalker will expect to
# see it:
len_ = endverbpos - pos
argd = ParsedVerbatimArgs(
verbatim_chars_node=w.make_node(
latexwalker.LatexCharsNode,
parsing_state=parsing_state,
chars=w.s[pos : pos + len_],
pos=pos,
len=len_,
)
)
return (argd, pos, len_)
if self.verbatim_arg_type == "verb-macro":
# read the next nonwhitespace char. This is the delimiter of the
# argument
while w.s[pos].isspace():
pos += 1
if pos >= len(w.s):
raise latexwalker.LatexWalkerParseError(
s=w.s, pos=pos, msg=r"Missing argument to \verb command"
)
verbdelimchar = w.s[pos]
beginpos = pos + 1
endpos = w.s.find(verbdelimchar, beginpos)
if endpos == -1:
raise latexwalker.LatexWalkerParseError(
s=w.s,
pos=pos,
msg=r"End of stream reached while reading argument to \verb command",
)
verbarg = w.s[beginpos:endpos]
argd = ParsedVerbatimArgs(
verbatim_chars_node=w.make_node(
latexwalker.LatexCharsNode,
parsing_state=parsing_state,
chars=verbarg,
pos=beginpos,
len=endpos - beginpos,
),
verbatim_delimiters=(verbdelimchar, verbdelimchar),
)
return (argd, pos, endpos + 1 - pos) # include delimiters in pos/len
def __repr__(self):
return "{}(verbatim_arg_type={!r})".format(
self.__class__.__name__, self.verbatim_arg_type
)