Files
context-continuity/venv.old-py38/lib/python3.9/site-packages/latex2mathml/walker.py
T

462 lines
20 KiB
Python
Raw Normal View History

from typing import Any, Iterator, NamedTuple, Optional
from latex2mathml import commands
from latex2mathml.exceptions import (
DenominatorNotFoundError,
DoubleSubscriptsError,
DoubleSuperscriptsError,
ExtraLeftOrMissingRightError,
InvalidAlignmentError,
InvalidStyleForGenfracError,
InvalidWidthError,
LimitsMustFollowMathOperatorError,
MissingEndError,
MissingSuperScriptOrSubscriptError,
NoAvailableTokensError,
NumeratorNotFoundError,
)
from latex2mathml.symbols_parser import convert_symbol
from latex2mathml.tokenizer import tokenize
class Node(NamedTuple):
token: str
children: Optional[tuple[Any, ...]] = None
delimiter: Optional[str] = None
alignment: Optional[str] = None
text: Optional[str] = None
attributes: Optional[dict[str, str]] = None
modifier: Optional[str] = None
def walk(data: str, display: str = "inline") -> list[Node]:
tokens = tokenize(data)
block = display == "block"
return _walk(tokens, block=block)
def _walk(tokens: Iterator[str], terminator: Optional[str] = None, limit: int = 0, block: bool = False) -> list[Node]:
group: list[Node] = []
token: str
has_available_tokens = False
for token in tokens:
has_available_tokens = True
if token == terminator:
delimiter = None
if terminator == commands.RIGHT:
delimiter = next(tokens)
group.append(Node(token=token, delimiter=delimiter))
break
elif (token == commands.RIGHT != terminator) or (token == commands.MIDDLE and terminator != commands.RIGHT):
raise ExtraLeftOrMissingRightError
elif token == commands.LEFT:
delimiter = next(tokens)
children = tuple(_walk(tokens, terminator=commands.RIGHT)) # make \right as a child of \left
if len(children) == 0 or children[-1].token != commands.RIGHT:
raise ExtraLeftOrMissingRightError
node = Node(token=token, children=children if len(children) else None, delimiter=delimiter)
elif token == commands.OPENING_BRACE:
children = tuple(_walk(tokens, terminator=commands.CLOSING_BRACE))
if len(children) and children[-1].token == commands.CLOSING_BRACE:
children = children[:-1]
node = Node(token=commands.BRACES, children=children)
elif token in (commands.SUBSCRIPT, commands.SUPERSCRIPT):
try:
previous = group.pop()
except IndexError:
previous = Node(token="") # left operand can be empty if not present
if token == previous.token == commands.SUBSCRIPT:
raise DoubleSubscriptsError
if (token == previous.token == commands.SUPERSCRIPT) and (
previous.children is not None
and len(previous.children) >= 2
and previous.children[1].token != commands.PRIME
):
raise DoubleSuperscriptsError
modifier = None
if previous.token == commands.LIMITS:
modifier = commands.LIMITS
try:
previous = group.pop()
if not previous.token.startswith("\\"): # TODO: Complete list of operators
raise LimitsMustFollowMathOperatorError
except IndexError:
raise LimitsMustFollowMathOperatorError
elif block and previous.token in (commands.SUMMATION, commands.PRODUCT):
# block summation and product should result in limited sub/sup
modifier = commands.LIMITS
if token == commands.SUBSCRIPT and previous.token == commands.SUPERSCRIPT and previous.children is not None:
children = tuple(_walk(tokens, terminator=terminator, limit=1))
node = Node(
token=commands.SUBSUP,
children=(previous.children[0], *children, previous.children[1]),
modifier=previous.modifier,
)
elif (
token == commands.SUPERSCRIPT and previous.token == commands.SUBSCRIPT and previous.children is not None
):
children = tuple(_walk(tokens, terminator=terminator, limit=1))
node = Node(token=commands.SUBSUP, children=(*previous.children, *children), modifier=previous.modifier)
elif (
token == commands.SUPERSCRIPT
and previous.token == commands.SUPERSCRIPT
and previous.children is not None
and previous.children[1].token == commands.PRIME
):
children = tuple(_walk(tokens, terminator=terminator, limit=1))
node = Node(
token=commands.SUPERSCRIPT,
children=(
previous.children[0],
Node(token=commands.BRACES, children=(previous.children[1], *children)),
),
modifier=previous.modifier,
)
else:
try:
children = tuple(_walk(tokens, terminator=terminator, limit=1))
except NoAvailableTokensError:
raise MissingSuperScriptOrSubscriptError
if previous.token in (commands.OVERBRACE, commands.UNDERBRACE):
modifier = previous.token
node = Node(token=token, children=(previous, *children), modifier=modifier)
elif token == commands.APOSTROPHE:
try:
previous = group.pop()
except IndexError:
previous = Node(token="") # left operand can be empty if not present
if (
previous.token == commands.SUPERSCRIPT
and previous.children is not None
and len(previous.children) >= 2
and previous.children[1].token != commands.PRIME
):
raise DoubleSuperscriptsError
if (
previous.token == commands.SUPERSCRIPT
and previous.children is not None
and len(previous.children) >= 2
and previous.children[1].token == commands.PRIME
):
node = Node(token=commands.SUPERSCRIPT, children=(previous.children[0], Node(token=commands.DPRIME)))
elif previous.token == commands.SUBSCRIPT and previous.children is not None:
node = Node(
token=commands.SUBSUP,
children=(*previous.children, Node(token=commands.PRIME)),
modifier=previous.modifier,
)
else:
node = Node(token=commands.SUPERSCRIPT, children=(previous, Node(token=commands.PRIME)))
elif token in commands.COMMANDS_WITH_TWO_PARAMETERS:
attributes = None
children = tuple(_walk(tokens, terminator=terminator, limit=2))
if token in (commands.OVERSET, commands.UNDERSET):
children = children[::-1]
node = Node(token=token, children=children, attributes=attributes)
elif token in commands.COMMANDS_WITH_ONE_PARAMETER or token.startswith(commands.MATH):
children = tuple(_walk(tokens, terminator=terminator, limit=1))
node = Node(token=token, children=children)
elif token == commands.NOT:
try:
next_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
if next_node.token.startswith("\\"):
negated_symbol = r"\n" + next_node.token[1:]
symbol = convert_symbol(negated_symbol)
if symbol:
node = Node(token=negated_symbol)
group.append(node)
continue
node = Node(token=token)
group.extend((node, next_node))
continue
except NoAvailableTokensError:
node = Node(token=token)
elif token in (commands.XLEFTARROW, commands.XRIGHTARROW):
children = tuple(_walk(tokens, terminator=terminator, limit=1))
if children[0].token == commands.OPENING_BRACKET:
children = (
Node(
token=commands.BRACES, children=tuple(_walk(tokens, terminator=commands.CLOSING_BRACKET))[:-1]
),
*tuple(_walk(tokens, terminator=terminator, limit=1)),
)
node = Node(token=token, children=children)
elif token in (commands.HSKIP, commands.HSPACE, commands.KERN, commands.MKERN, commands.MSKIP, commands.MSPACE):
children = tuple(_walk(tokens, terminator=terminator, limit=1))
if children[0].token == commands.BRACES and children[0].children is not None:
children = children[0].children
node = Node(token=token, attributes={"width": children[0].token})
elif token == commands.COLOR:
attributes = {"mathcolor": next(tokens)}
children = tuple(_walk(tokens, terminator=terminator))
sibling = None
if len(children) and children[-1].token == terminator:
children, sibling = children[:-1], children[-1]
group.append(Node(token=token, children=children, attributes=attributes))
if sibling:
group.append(sibling)
break
elif token == commands.STYLE:
attributes = {"style": next(tokens)}
next_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
node = next_node._replace(attributes=attributes)
elif token in (
*commands.BIG.keys(),
*commands.BIG_OPEN_CLOSE.keys(),
commands.FBOX,
commands.HBOX,
commands.MBOX,
commands.MIDDLE,
commands.TEXT,
commands.TEXTBF,
commands.TEXTIT,
commands.TEXTRM,
commands.TEXTSF,
commands.TEXTTT,
):
node = Node(token=token, text=next(tokens))
elif token == commands.HREF:
attributes = {"href": next(tokens)}
children = tuple(_walk(tokens, terminator=terminator, limit=1))
node = Node(token=token, children=children, attributes=attributes)
elif token in (
commands.ABOVE,
commands.ATOP,
commands.ABOVEWITHDELIMS,
commands.ATOPWITHDELIMS,
commands.BRACE,
commands.BRACK,
commands.CHOOSE,
commands.OVER,
):
attributes = None
delimiter = None
if token == commands.ABOVEWITHDELIMS:
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
elif token == commands.ATOPWITHDELIMS:
attributes = {"linethickness": "0"}
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
elif token == commands.BRACE:
delimiter = "{}"
elif token == commands.BRACK:
delimiter = "[]"
elif token == commands.CHOOSE:
delimiter = "()"
if token in (commands.ABOVE, commands.ABOVEWITHDELIMS):
dimension_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
dimension = _get_dimension(dimension_node)
attributes = {"linethickness": dimension}
elif token in (commands.ATOP, commands.BRACE, commands.BRACK, commands.CHOOSE):
attributes = {"linethickness": "0"}
denominator = tuple(_walk(tokens, terminator=terminator))
sibling = None
if len(denominator) and denominator[-1].token == terminator:
denominator, sibling = denominator[:-1], denominator[-1]
if len(denominator) == 0:
if token in (commands.BRACE, commands.BRACK):
denominator = (Node(token=commands.BRACES, children=()),)
else:
raise DenominatorNotFoundError
if len(group) == 0:
if token in (commands.BRACE, commands.BRACK):
group = [Node(token=commands.BRACES, children=())]
else:
raise NumeratorNotFoundError
if len(denominator) > 1:
denominator = (Node(token=commands.BRACES, children=denominator),)
if len(group) == 1:
children = (*group, *denominator)
else:
children = (Node(token=commands.BRACES, children=tuple(group)), *denominator)
group = [Node(token=commands.FRAC, children=children, attributes=attributes, delimiter=delimiter)]
if sibling is not None:
group.append(sibling)
break
elif token == commands.SQRT:
root_nodes = None
next_node = tuple(_walk(tokens, limit=1))[0]
if next_node.token == commands.OPENING_BRACKET:
root_nodes = tuple(_walk(tokens, terminator=commands.CLOSING_BRACKET))[:-1]
next_node = tuple(_walk(tokens, limit=1))[0]
if len(root_nodes) > 1:
root_nodes = (Node(token=commands.BRACES, children=root_nodes),)
if root_nodes:
node = Node(token=commands.ROOT, children=(next_node, *root_nodes))
else:
node = Node(token=token, children=(next_node,))
elif token == commands.ROOT:
root_nodes = tuple(_walk(tokens, terminator=r"\of"))[:-1]
next_node = tuple(_walk(tokens, limit=1))[0]
if len(root_nodes) > 1:
root_nodes = (Node(token=commands.BRACES, children=root_nodes),)
if root_nodes:
node = Node(token=token, children=(next_node, *root_nodes))
else:
node = Node(token=token, children=(next_node, Node(token=commands.BRACES, children=())))
elif token in commands.MATRICES:
children = tuple(_walk(tokens, terminator=terminator))
sibling = None
if len(children) and children[-1].token == terminator:
children, sibling = children[:-1], children[-1]
if len(children) == 1 and children[0].token == commands.BRACES and children[0].children:
children = children[0].children
if sibling is not None:
group.extend([Node(token=token, children=children, alignment=""), sibling])
break
else:
node = Node(token=token, children=children, alignment="")
elif token == commands.GENFRAC:
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
dimension_node, style_node = tuple(_walk(tokens, terminator=terminator, limit=2))
dimension = _get_dimension(dimension_node)
style = _get_style(style_node)
attributes = {"linethickness": dimension}
children = tuple(_walk(tokens, terminator=terminator, limit=2))
group.extend(
[Node(token=style), Node(token=token, children=children, delimiter=delimiter, attributes=attributes)]
)
break
elif token == commands.SIDESET:
left, right, operator = tuple(_walk(tokens, terminator=terminator, limit=3))
left_token, left_children = _make_subsup(left)
right_token, right_children = _make_subsup(right)
attributes = {"movablelimits": "false"}
node = Node(
token=token,
children=(
Node(
token=left_token,
children=(
Node(
token=commands.VPHANTOM,
children=(
Node(token=operator.token, children=operator.children, attributes=attributes),
),
),
*left_children,
),
),
Node(
token=right_token,
children=(
Node(token=operator.token, children=operator.children, attributes=attributes),
*right_children,
),
),
),
)
elif token == commands.SKEW:
width_node, child = tuple(_walk(tokens, terminator=terminator, limit=2))
width = width_node.token
if width == commands.BRACES:
if width_node.children is None or len(width_node.children) == 0:
raise InvalidWidthError
width = width_node.children[0].token
if not width.isdigit():
raise InvalidWidthError
node = Node(token=token, children=(child,), attributes={"width": f"{0.0555 * int(width):.3f}em"})
elif token.startswith(commands.BEGIN):
node = _get_environment_node(token, tokens)
else:
node = Node(token=token)
group.append(node)
if limit and len(group) >= limit:
break
if not has_available_tokens:
raise NoAvailableTokensError
return group
def _make_subsup(node: Node) -> tuple[str, tuple[Node, ...]]:
# TODO: raise error instead of assertion
assert node.token == commands.BRACES
try:
assert (
node.children is not None
and 2 <= len(node.children[0].children) <= 3
and node.children[0].token
in (
commands.SUBSUP,
commands.SUBSCRIPT,
commands.SUPERSCRIPT,
)
)
token = node.children[0].token
children = node.children[0].children[1:]
return token, children
except IndexError:
return "", ()
def _get_dimension(node: Node) -> str:
dimension = node.token
if node.token == commands.BRACES and node.children is not None:
dimension = node.children[0].token
return dimension
def _get_style(node: Node) -> str:
style = node.token
if node.token == commands.BRACES and node.children is not None:
style = node.children[0].token
if style == "0":
return commands.DISPLAYSTYLE
if style == "1":
return commands.TEXTSTYLE
if style == "2":
return commands.SCRIPTSTYLE
if style == "3":
return commands.SCRIPTSCRIPTSTYLE
raise InvalidStyleForGenfracError
def _get_environment_node(token: str, tokens: Iterator[str]) -> Node:
# TODO: support non-matrix environments
start_index = token.index("{") + 1
environment = token[start_index:-1]
terminator = rf"{commands.END}{{{environment}}}"
children = tuple(_walk(tokens, terminator=terminator))
if len(children) and children[-1].token != terminator:
raise MissingEndError
children = children[:-1]
alignment = ""
if len(children) and children[0].token == commands.OPENING_BRACKET:
children_iter = iter(children)
next(children_iter) # remove BRACKET
for c in children_iter:
if c.token == commands.CLOSING_BRACKET:
break
elif c.token not in "lcr|":
raise InvalidAlignmentError
alignment += c.token
children = tuple(children_iter)
elif (
len(children)
and children[0].children is not None
and (
children[0].token == commands.BRACES
or (environment.endswith("*") and children[0].token == commands.BRACKETS)
)
and all(c.token in "lcr|" for c in children[0].children)
):
alignment = "".join(c.token for c in children[0].children)
children = children[1:]
return Node(token=rf"\{environment}", children=children, alignment=alignment)