497 lines
19 KiB
Python
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
|
|
)
|