596 lines
23 KiB
Python
596 lines
23 KiB
Python
|
|
import copy
|
||
|
|
import enum
|
||
|
|
import re
|
||
|
|
from collections import OrderedDict
|
||
|
|
from typing import Iterable, Iterator, Optional
|
||
|
|
from xml.etree.cElementTree import Element, SubElement, tostring
|
||
|
|
from xml.sax.saxutils import unescape
|
||
|
|
|
||
|
|
from latex2mathml import commands
|
||
|
|
from latex2mathml.symbols_parser import convert_symbol
|
||
|
|
from latex2mathml.walker import Node, walk
|
||
|
|
|
||
|
|
COLUMN_ALIGNMENT_MAP = {"r": "right", "l": "left", "c": "center"}
|
||
|
|
OPERATORS = (
|
||
|
|
"+",
|
||
|
|
"-",
|
||
|
|
"*",
|
||
|
|
"/",
|
||
|
|
"(",
|
||
|
|
")",
|
||
|
|
"=",
|
||
|
|
",",
|
||
|
|
"?",
|
||
|
|
"[",
|
||
|
|
"]",
|
||
|
|
"|",
|
||
|
|
r"\|",
|
||
|
|
"!",
|
||
|
|
r"\{",
|
||
|
|
r"\}",
|
||
|
|
r">",
|
||
|
|
r"<",
|
||
|
|
r".",
|
||
|
|
r"\bigotimes",
|
||
|
|
r"\centerdot",
|
||
|
|
r"\dots",
|
||
|
|
r"\dotsc",
|
||
|
|
r"\dotso",
|
||
|
|
r"\gt",
|
||
|
|
r"\ldotp",
|
||
|
|
r"\lt",
|
||
|
|
r"\lvert",
|
||
|
|
r"\lVert",
|
||
|
|
r"\lvertneqq",
|
||
|
|
r"\ngeqq",
|
||
|
|
r"\omicron",
|
||
|
|
r"\rvert",
|
||
|
|
r"\rVert",
|
||
|
|
r"\S",
|
||
|
|
r"\smallfrown",
|
||
|
|
r"\smallint",
|
||
|
|
r"\smallsmile",
|
||
|
|
r"\surd",
|
||
|
|
r"\varsubsetneqq",
|
||
|
|
r"\varsupsetneqq",
|
||
|
|
)
|
||
|
|
MATH_MODE_PATTERN = re.compile(r"\\\$|\$|\\?[^\\$]+")
|
||
|
|
|
||
|
|
|
||
|
|
class Mode(enum.Enum):
|
||
|
|
TEXT = enum.auto()
|
||
|
|
MATH = enum.auto()
|
||
|
|
|
||
|
|
|
||
|
|
def convert(
|
||
|
|
latex: str,
|
||
|
|
xmlns: str = "http://www.w3.org/1998/Math/MathML",
|
||
|
|
display: str = "inline",
|
||
|
|
parent: Optional[Element] = None,
|
||
|
|
) -> str:
|
||
|
|
math = convert_to_element(latex, xmlns, display, parent)
|
||
|
|
return _convert(math)
|
||
|
|
|
||
|
|
|
||
|
|
def convert_to_element(
|
||
|
|
latex: str,
|
||
|
|
xmlns: str = "http://www.w3.org/1998/Math/MathML",
|
||
|
|
display: str = "inline",
|
||
|
|
parent: Optional[Element] = None,
|
||
|
|
) -> Element:
|
||
|
|
tag = "math"
|
||
|
|
attrib = {"xmlns": xmlns, "display": display}
|
||
|
|
math = Element(tag, attrib) if parent is None else SubElement(parent, tag, attrib)
|
||
|
|
row = SubElement(math, "mrow")
|
||
|
|
_convert_group(iter(walk(latex, display)), row)
|
||
|
|
return math
|
||
|
|
|
||
|
|
|
||
|
|
def _convert(tree: Element) -> str:
|
||
|
|
return unescape(tostring(tree, encoding="unicode"))
|
||
|
|
|
||
|
|
|
||
|
|
def _convert_matrix(nodes: Iterator[Node], parent: Element, command: str, alignment: Optional[str] = None) -> None:
|
||
|
|
row = None
|
||
|
|
cell = None
|
||
|
|
|
||
|
|
col_index = 0
|
||
|
|
col_alignment = None
|
||
|
|
|
||
|
|
max_col_size = 0
|
||
|
|
|
||
|
|
row_index = 0
|
||
|
|
row_lines = []
|
||
|
|
|
||
|
|
hfil_indexes: list[bool] = []
|
||
|
|
|
||
|
|
for node in nodes:
|
||
|
|
if row is None:
|
||
|
|
row = SubElement(parent, "mtr")
|
||
|
|
|
||
|
|
if cell is None:
|
||
|
|
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||
|
|
cell = _make_matrix_cell(row, col_alignment)
|
||
|
|
|
||
|
|
if node.token == commands.BRACES:
|
||
|
|
_convert_group(iter([node]), cell)
|
||
|
|
elif node.token == "&":
|
||
|
|
_set_cell_alignment(cell, hfil_indexes)
|
||
|
|
hfil_indexes = []
|
||
|
|
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||
|
|
cell = _make_matrix_cell(row, col_alignment)
|
||
|
|
if command in (commands.SPLIT, commands.ALIGN) and col_index % 2 == 0:
|
||
|
|
SubElement(cell, "mi")
|
||
|
|
elif node.token in (commands.DOUBLEBACKSLASH, commands.CARRIAGE_RETURN):
|
||
|
|
_set_cell_alignment(cell, hfil_indexes)
|
||
|
|
hfil_indexes = []
|
||
|
|
row_index += 1
|
||
|
|
if col_index > max_col_size:
|
||
|
|
max_col_size = col_index
|
||
|
|
col_index = 0
|
||
|
|
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||
|
|
row = SubElement(parent, "mtr")
|
||
|
|
cell = _make_matrix_cell(row, col_alignment)
|
||
|
|
elif node.token == commands.HLINE:
|
||
|
|
row_lines.append("solid")
|
||
|
|
elif node.token == commands.HDASHLINE:
|
||
|
|
row_lines.append("dashed")
|
||
|
|
elif node.token == commands.HFIL:
|
||
|
|
hfil_indexes.append(True)
|
||
|
|
else:
|
||
|
|
if row_index > len(row_lines):
|
||
|
|
row_lines.append("none")
|
||
|
|
hfil_indexes.append(False)
|
||
|
|
_convert_group(iter([node]), cell)
|
||
|
|
|
||
|
|
if col_index > max_col_size:
|
||
|
|
max_col_size = col_index
|
||
|
|
|
||
|
|
if any(r == "solid" for r in row_lines):
|
||
|
|
parent.set("rowlines", " ".join(row_lines))
|
||
|
|
|
||
|
|
if row is not None and cell is not None and len(cell) == 0:
|
||
|
|
# Remove last row if it does not contain anything
|
||
|
|
parent.remove(row)
|
||
|
|
|
||
|
|
if max_col_size and command == commands.ALIGN:
|
||
|
|
spacing = ("0em", "2em")
|
||
|
|
multiplier = max_col_size // len(spacing)
|
||
|
|
parent.set("columnspacing", " ".join(spacing * multiplier))
|
||
|
|
|
||
|
|
|
||
|
|
def _set_cell_alignment(cell: Element, hfil_indexes: list[bool]) -> None:
|
||
|
|
if cell is not None and any(hfil_indexes) and len(hfil_indexes) > 1:
|
||
|
|
if hfil_indexes[0] and not hfil_indexes[-1]:
|
||
|
|
cell.attrib["columnalign"] = "right"
|
||
|
|
elif not hfil_indexes[0] and hfil_indexes[-1]:
|
||
|
|
cell.attrib["columnalign"] = "left"
|
||
|
|
|
||
|
|
|
||
|
|
def _get_column_alignment(
|
||
|
|
alignment: Optional[str], column_alignment: Optional[str], column_index: int
|
||
|
|
) -> tuple[Optional[str], int]:
|
||
|
|
if alignment:
|
||
|
|
try:
|
||
|
|
column_alignment = COLUMN_ALIGNMENT_MAP.get(alignment[column_index])
|
||
|
|
except IndexError:
|
||
|
|
column_alignment = COLUMN_ALIGNMENT_MAP.get(alignment[column_index % len(alignment)])
|
||
|
|
column_index += 1
|
||
|
|
return column_alignment, column_index
|
||
|
|
|
||
|
|
|
||
|
|
def _make_matrix_cell(row: Element, column_alignment: Optional[str]) -> Element:
|
||
|
|
if column_alignment:
|
||
|
|
return SubElement(row, "mtd", columnalign=column_alignment)
|
||
|
|
return SubElement(row, "mtd")
|
||
|
|
|
||
|
|
|
||
|
|
def _convert_group(nodes: Iterable[Node], parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||
|
|
_font = font
|
||
|
|
for node in nodes:
|
||
|
|
token = node.token
|
||
|
|
if token in (*commands.MSTYLE_SIZES, *commands.STYLES):
|
||
|
|
node = Node(token=token, children=tuple(n for n in nodes))
|
||
|
|
_convert_command(node, parent, _font)
|
||
|
|
elif token in commands.CONVERSION_MAP or token in (commands.MOD, commands.PMOD):
|
||
|
|
_convert_command(node, parent, _font)
|
||
|
|
elif token in commands.LOCAL_FONTS and node.children is not None:
|
||
|
|
_convert_group(iter(node.children), parent, commands.LOCAL_FONTS[token])
|
||
|
|
elif token.startswith(commands.MATH) and node.children is not None:
|
||
|
|
_convert_group(iter(node.children), parent, _font)
|
||
|
|
elif token in commands.GLOBAL_FONTS.keys():
|
||
|
|
_font = commands.GLOBAL_FONTS.get(token)
|
||
|
|
elif node.children is None:
|
||
|
|
_convert_symbol(node, parent, _font)
|
||
|
|
elif node.children is not None:
|
||
|
|
attributes = node.attributes or {}
|
||
|
|
_row = SubElement(parent, "mrow", attrib=attributes)
|
||
|
|
_convert_group(iter(node.children), _row, _font)
|
||
|
|
|
||
|
|
|
||
|
|
def _get_alignment_and_column_lines(alignment: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
|
||
|
|
if alignment is None:
|
||
|
|
return None, None
|
||
|
|
if "|" not in alignment:
|
||
|
|
return alignment, None
|
||
|
|
_alignment = ""
|
||
|
|
column_lines = []
|
||
|
|
for c in alignment:
|
||
|
|
if c == "|":
|
||
|
|
column_lines.append("solid")
|
||
|
|
else:
|
||
|
|
_alignment += c
|
||
|
|
if len(_alignment) - len(column_lines) == 2:
|
||
|
|
column_lines.append("none")
|
||
|
|
return _alignment, " ".join(column_lines)
|
||
|
|
|
||
|
|
|
||
|
|
def separate_by_mode(text: str) -> Iterator[tuple[str, Mode]]:
|
||
|
|
string = ""
|
||
|
|
is_math_mode = False
|
||
|
|
for match in MATH_MODE_PATTERN.findall(text):
|
||
|
|
if match == "$": # should match both $ and $$
|
||
|
|
yield string, Mode.MATH if is_math_mode else Mode.TEXT
|
||
|
|
string = ""
|
||
|
|
is_math_mode = not is_math_mode
|
||
|
|
else:
|
||
|
|
string += match
|
||
|
|
if len(string):
|
||
|
|
yield string, Mode.MATH if is_math_mode else Mode.TEXT
|
||
|
|
# TODO: if stays in math mode, means not terminated properly, raise error
|
||
|
|
|
||
|
|
|
||
|
|
def _convert_command(node: Node, parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||
|
|
command = node.token
|
||
|
|
modifier = node.modifier
|
||
|
|
|
||
|
|
if command in (commands.SUBSTACK, commands.SMALLMATRIX):
|
||
|
|
parent = SubElement(parent, "mstyle", scriptlevel="1")
|
||
|
|
elif command == commands.CASES:
|
||
|
|
parent = SubElement(parent, "mrow")
|
||
|
|
lbrace = SubElement(parent, "mo", OrderedDict([("stretchy", "true"), ("fence", "true"), ("form", "prefix")]))
|
||
|
|
lbrace.text = "&#x{};".format(convert_symbol(commands.LBRACE))
|
||
|
|
elif command in (commands.DBINOM, commands.DFRAC):
|
||
|
|
parent = SubElement(parent, "mstyle", displaystyle="true", scriptlevel="0")
|
||
|
|
elif command == commands.HPHANTOM:
|
||
|
|
parent = SubElement(parent, "mpadded", height="0", depth="0")
|
||
|
|
elif command == commands.VPHANTOM:
|
||
|
|
parent = SubElement(parent, "mpadded", width="0")
|
||
|
|
elif command in (commands.TBINOM, commands.HBOX, commands.MBOX, commands.TFRAC):
|
||
|
|
parent = SubElement(parent, "mstyle", displaystyle="false", scriptlevel="0")
|
||
|
|
elif command in (commands.MOD, commands.PMOD):
|
||
|
|
SubElement(parent, "mspace", width="1em")
|
||
|
|
|
||
|
|
tag, attributes = copy.deepcopy(commands.CONVERSION_MAP[command])
|
||
|
|
|
||
|
|
if node.attributes is not None and node.token != commands.SKEW:
|
||
|
|
attributes.update(node.attributes)
|
||
|
|
|
||
|
|
if command == commands.LEFT:
|
||
|
|
parent = SubElement(parent, "mrow")
|
||
|
|
|
||
|
|
_append_prefix_element(node, parent)
|
||
|
|
|
||
|
|
alignment, column_lines = _get_alignment_and_column_lines(node.alignment)
|
||
|
|
|
||
|
|
if column_lines:
|
||
|
|
attributes["columnlines"] = column_lines
|
||
|
|
|
||
|
|
if command == commands.SUBSUP and node.children is not None and node.children[0].token == commands.GCD:
|
||
|
|
tag = "munderover"
|
||
|
|
elif command == commands.SUPERSCRIPT and modifier in (commands.LIMITS, commands.OVERBRACE):
|
||
|
|
tag = "mover"
|
||
|
|
elif command == commands.SUBSCRIPT and modifier in (commands.LIMITS, commands.UNDERBRACE):
|
||
|
|
tag = "munder"
|
||
|
|
elif command == commands.SUBSUP and modifier in (commands.LIMITS, commands.OVERBRACE, commands.UNDERBRACE):
|
||
|
|
tag = "munderover"
|
||
|
|
elif (
|
||
|
|
command in (commands.XLEFTARROW, commands.XRIGHTARROW) and node.children is not None and len(node.children) == 2
|
||
|
|
):
|
||
|
|
tag = "munderover"
|
||
|
|
|
||
|
|
element = SubElement(parent, tag, attributes)
|
||
|
|
|
||
|
|
if command in commands.LIMIT:
|
||
|
|
element.text = command[1:]
|
||
|
|
elif command in (commands.MOD, commands.PMOD):
|
||
|
|
element.text = "mod"
|
||
|
|
SubElement(parent, "mspace", width="0.333em")
|
||
|
|
elif command == commands.BMOD:
|
||
|
|
element.text = "mod"
|
||
|
|
elif command in (commands.XLEFTARROW, commands.XRIGHTARROW):
|
||
|
|
style = SubElement(element, "mstyle", scriptlevel="0")
|
||
|
|
arrow = SubElement(style, "mo")
|
||
|
|
if command == commands.XLEFTARROW:
|
||
|
|
arrow.text = "←"
|
||
|
|
elif command == commands.XRIGHTARROW:
|
||
|
|
arrow.text = "→"
|
||
|
|
elif node.text is not None:
|
||
|
|
if command == commands.MIDDLE:
|
||
|
|
element.text = "&#x{};".format(convert_symbol(node.text))
|
||
|
|
elif command == commands.HBOX:
|
||
|
|
mtext: Optional[Element] = element
|
||
|
|
for text, mode in separate_by_mode(node.text):
|
||
|
|
if mode == Mode.TEXT:
|
||
|
|
if mtext is None:
|
||
|
|
mtext = SubElement(parent, tag, attributes)
|
||
|
|
mtext.text = text.replace(" ", " ")
|
||
|
|
_set_font(mtext, "mtext", font)
|
||
|
|
mtext = None
|
||
|
|
else:
|
||
|
|
_row = SubElement(parent, "mrow")
|
||
|
|
_convert_group(iter(walk(text)), _row)
|
||
|
|
else:
|
||
|
|
if command == commands.FBOX:
|
||
|
|
element = SubElement(element, "mtext")
|
||
|
|
element.text = node.text.replace(" ", " ")
|
||
|
|
_set_font(element, "mtext", font)
|
||
|
|
elif node.delimiter is not None and command not in (commands.FRAC, commands.GENFRAC):
|
||
|
|
if node.delimiter != ".":
|
||
|
|
symbol = convert_symbol(node.delimiter)
|
||
|
|
element.text = node.delimiter if symbol is None else "&#x{};".format(symbol)
|
||
|
|
|
||
|
|
if node.children is not None:
|
||
|
|
_parent = element
|
||
|
|
if command in (commands.LEFT, commands.MOD, commands.PMOD):
|
||
|
|
_parent = parent
|
||
|
|
if command in commands.MATRICES:
|
||
|
|
if command == commands.CASES:
|
||
|
|
alignment = "l"
|
||
|
|
elif command in (commands.SPLIT, commands.ALIGN):
|
||
|
|
alignment = "rl"
|
||
|
|
_convert_matrix(iter(node.children), _parent, command, alignment=alignment)
|
||
|
|
elif command == commands.CFRAC:
|
||
|
|
for child in node.children:
|
||
|
|
p = SubElement(_parent, "mstyle", displaystyle="false", scriptlevel="0")
|
||
|
|
_convert_group(iter([child]), p, font)
|
||
|
|
elif command == commands.SIDESET:
|
||
|
|
Node(
|
||
|
|
r"\style",
|
||
|
|
children=(Node(r"\mspace", attributes={"width": "-0.167em"}),),
|
||
|
|
attributes={"scriptlevel": "0"},
|
||
|
|
),
|
||
|
|
left, right = node.children
|
||
|
|
_convert_group(iter([left]), _parent, font)
|
||
|
|
fill = SubElement(_parent, "mstyle", scriptlevel="0")
|
||
|
|
SubElement(fill, "mspace", width="-0.167em")
|
||
|
|
_convert_group(iter([right]), _parent, font)
|
||
|
|
elif command == commands.SKEW:
|
||
|
|
child = node.children[0]
|
||
|
|
new_node = Node(
|
||
|
|
token=child.token,
|
||
|
|
children=(
|
||
|
|
Node(
|
||
|
|
token=commands.BRACES,
|
||
|
|
children=(*child.children, Node(token=commands.MKERN, attributes=node.attributes)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
_convert_group(iter([new_node]), _parent, font)
|
||
|
|
elif command in (commands.XLEFTARROW, commands.XRIGHTARROW):
|
||
|
|
for child in node.children:
|
||
|
|
padded = SubElement(
|
||
|
|
_parent,
|
||
|
|
"mpadded",
|
||
|
|
OrderedDict(
|
||
|
|
[("width", "+0.833em"), ("lspace", "0.556em"), ("voffset", "-.2em"), ("height", "-.2em")]
|
||
|
|
),
|
||
|
|
)
|
||
|
|
_convert_group(iter([child]), padded, font)
|
||
|
|
SubElement(padded, "mspace", depth=".25em")
|
||
|
|
else:
|
||
|
|
_convert_group(iter(node.children), _parent, font)
|
||
|
|
|
||
|
|
_add_diacritic(command, element)
|
||
|
|
|
||
|
|
_append_postfix_element(node, parent)
|
||
|
|
|
||
|
|
|
||
|
|
def _add_diacritic(command: str, parent: Element) -> None:
|
||
|
|
if command in commands.DIACRITICS:
|
||
|
|
text, attributes = copy.deepcopy(commands.DIACRITICS[command])
|
||
|
|
element = SubElement(parent, "mo", attributes)
|
||
|
|
element.text = text
|
||
|
|
|
||
|
|
|
||
|
|
def _convert_and_append_command(command: str, parent: Element, attributes: Optional[dict[str, str]] = None) -> None:
|
||
|
|
code_point = convert_symbol(command)
|
||
|
|
mo = SubElement(parent, "mo", attributes if attributes is not None else {})
|
||
|
|
mo.text = "&#x{};".format(code_point) if code_point else command
|
||
|
|
|
||
|
|
|
||
|
|
def _append_prefix_element(node: Node, parent: Element) -> None:
|
||
|
|
size = "2.047em"
|
||
|
|
if parent.attrib.get("displaystyle") == "false" or node.token == commands.TBINOM:
|
||
|
|
size = "1.2em"
|
||
|
|
if node.token in (r"\pmatrix", commands.PMOD):
|
||
|
|
_convert_and_append_command(r"\lparen", parent)
|
||
|
|
elif node.token in (commands.BINOM, commands.DBINOM, commands.TBINOM):
|
||
|
|
_convert_and_append_command(r"\lparen", parent, {"minsize": size, "maxsize": size})
|
||
|
|
elif node.token == r"\bmatrix":
|
||
|
|
_convert_and_append_command(r"\lbrack", parent)
|
||
|
|
elif node.token == r"\Bmatrix":
|
||
|
|
_convert_and_append_command(r"\lbrace", parent)
|
||
|
|
elif node.token == r"\vmatrix":
|
||
|
|
_convert_and_append_command(r"\vert", parent)
|
||
|
|
elif node.token == r"\Vmatrix":
|
||
|
|
_convert_and_append_command(r"\Vert", parent)
|
||
|
|
elif node.token in (commands.FRAC, commands.GENFRAC) and node.delimiter is not None and node.delimiter[0] != ".":
|
||
|
|
# TODO: use 1.2em if inline
|
||
|
|
_convert_and_append_command(node.delimiter[0], parent, {"minsize": size, "maxsize": size})
|
||
|
|
|
||
|
|
|
||
|
|
def _append_postfix_element(node: Node, parent: Element) -> None:
|
||
|
|
size = "2.047em"
|
||
|
|
if parent.attrib.get("displaystyle") == "false" or node.token == commands.TBINOM:
|
||
|
|
size = "1.2em"
|
||
|
|
if node.token in (r"\pmatrix", commands.PMOD):
|
||
|
|
_convert_and_append_command(r"\rparen", parent)
|
||
|
|
elif node.token in (commands.BINOM, commands.DBINOM, commands.TBINOM):
|
||
|
|
_convert_and_append_command(r"\rparen", parent, {"minsize": size, "maxsize": size})
|
||
|
|
elif node.token == r"\bmatrix":
|
||
|
|
_convert_and_append_command(r"\rbrack", parent)
|
||
|
|
elif node.token == r"\Bmatrix":
|
||
|
|
_convert_and_append_command(r"\rbrace", parent)
|
||
|
|
elif node.token == r"\vmatrix":
|
||
|
|
_convert_and_append_command(r"\vert", parent)
|
||
|
|
elif node.token == r"\Vmatrix":
|
||
|
|
_convert_and_append_command(r"\Vert", parent)
|
||
|
|
elif node.token in (commands.FRAC, commands.GENFRAC) and node.delimiter is not None and node.delimiter[1] != ".":
|
||
|
|
# TODO: use 1.2em if inline
|
||
|
|
_convert_and_append_command(node.delimiter[1], parent, {"minsize": size, "maxsize": size})
|
||
|
|
elif node.token == commands.SKEW and node.attributes is not None:
|
||
|
|
SubElement(parent, "mspace", width="-" + node.attributes["width"])
|
||
|
|
|
||
|
|
|
||
|
|
def _convert_symbol(node: Node, parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||
|
|
token = node.token
|
||
|
|
attributes = node.attributes or {}
|
||
|
|
symbol = convert_symbol(token)
|
||
|
|
if re.match(r"\d+(.\d+)?", token):
|
||
|
|
element = SubElement(parent, "mn", attrib=attributes)
|
||
|
|
element.text = token
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
elif token in OPERATORS:
|
||
|
|
element = SubElement(parent, "mo", attrib=attributes)
|
||
|
|
element.text = token if symbol is None else "&#x{};".format(symbol)
|
||
|
|
if token == r"\|":
|
||
|
|
element.attrib["fence"] = "false"
|
||
|
|
if token == r"\smallint":
|
||
|
|
element.attrib["largeop"] = "false"
|
||
|
|
if token in ("(", ")", "[", "]", "|", r"\|", r"\{", r"\}", r"\surd"):
|
||
|
|
element.attrib["stretchy"] = "false"
|
||
|
|
_set_font(element, "fence", font)
|
||
|
|
else:
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
elif (
|
||
|
|
symbol
|
||
|
|
and (
|
||
|
|
int(symbol, 16) in range(int("2200", 16), int("22FF", 16) + 1)
|
||
|
|
or int(symbol, 16) in range(int("2190", 16), int("21FF", 16) + 1)
|
||
|
|
)
|
||
|
|
or symbol == "."
|
||
|
|
):
|
||
|
|
element = SubElement(parent, "mo", attrib=attributes)
|
||
|
|
element.text = "&#x{};".format(symbol)
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
elif token in (r"\ ", "~", commands.NOBREAKSPACE, commands.SPACE):
|
||
|
|
element = SubElement(parent, "mtext", attrib=attributes)
|
||
|
|
element.text = " "
|
||
|
|
_set_font(element, "mtext", font)
|
||
|
|
elif token == commands.NOT:
|
||
|
|
mpadded = SubElement(parent, "mpadded", width="0")
|
||
|
|
element = SubElement(mpadded, "mtext")
|
||
|
|
element.text = "⧸"
|
||
|
|
elif token in (
|
||
|
|
commands.DETERMINANT,
|
||
|
|
commands.GCD,
|
||
|
|
commands.INTOP,
|
||
|
|
commands.INJLIM,
|
||
|
|
commands.LIMINF,
|
||
|
|
commands.LIMSUP,
|
||
|
|
commands.PR,
|
||
|
|
commands.PROJLIM,
|
||
|
|
):
|
||
|
|
element = SubElement(parent, "mo", attrib={"movablelimits": "true", **attributes})
|
||
|
|
texts = {
|
||
|
|
commands.INJLIM: "inj lim",
|
||
|
|
commands.INTOP: "∫",
|
||
|
|
commands.LIMINF: "lim inf",
|
||
|
|
commands.LIMSUP: "lim sup",
|
||
|
|
commands.PROJLIM: "proj lim",
|
||
|
|
}
|
||
|
|
element.text = texts.get(token, token[1:])
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
elif token == commands.IDOTSINT:
|
||
|
|
_parent = SubElement(parent, "mrow", attrib=attributes)
|
||
|
|
for s in ("∫", "⋯", "∫"):
|
||
|
|
element = SubElement(_parent, "mo")
|
||
|
|
element.text = s
|
||
|
|
elif token in (commands.LATEX, commands.TEX):
|
||
|
|
_parent = SubElement(parent, "mrow", attrib=attributes)
|
||
|
|
if token == commands.LATEX:
|
||
|
|
mi_l = SubElement(_parent, "mi")
|
||
|
|
mi_l.text = "L"
|
||
|
|
SubElement(_parent, "mspace", width="-.325em")
|
||
|
|
mpadded = SubElement(_parent, "mpadded", height="+.21ex", depth="-.21ex", voffset="+.21ex")
|
||
|
|
mstyle = SubElement(mpadded, "mstyle", displaystyle="false", scriptlevel="1")
|
||
|
|
mrow = SubElement(mstyle, "mrow")
|
||
|
|
mi_a = SubElement(mrow, "mi")
|
||
|
|
mi_a.text = "A"
|
||
|
|
SubElement(_parent, "mspace", width="-.17em")
|
||
|
|
_set_font(mi_l, mi_l.tag, font)
|
||
|
|
_set_font(mi_a, mi_a.tag, font)
|
||
|
|
mi_t = SubElement(_parent, "mi")
|
||
|
|
mi_t.text = "T"
|
||
|
|
SubElement(_parent, "mspace", width="-.14em")
|
||
|
|
mpadded = SubElement(_parent, "mpadded", height="-.5ex", depth="+.5ex", voffset="-.5ex")
|
||
|
|
mrow = SubElement(mpadded, "mrow")
|
||
|
|
mi_e = SubElement(mrow, "mi")
|
||
|
|
mi_e.text = "E"
|
||
|
|
SubElement(_parent, "mspace", width="-.115em")
|
||
|
|
mi_x = SubElement(_parent, "mi")
|
||
|
|
mi_x.text = "X"
|
||
|
|
_set_font(mi_t, mi_t.tag, font)
|
||
|
|
_set_font(mi_e, mi_e.tag, font)
|
||
|
|
_set_font(mi_x, mi_x.tag, font)
|
||
|
|
elif token.startswith(commands.OPERATORNAME):
|
||
|
|
element = SubElement(parent, "mo", attrib=attributes)
|
||
|
|
element.text = token[14:-1]
|
||
|
|
elif token.startswith(commands.BACKSLASH):
|
||
|
|
element = SubElement(parent, "mi", attrib=attributes)
|
||
|
|
if symbol:
|
||
|
|
element.text = "&#x{};".format(symbol)
|
||
|
|
elif token in commands.FUNCTIONS:
|
||
|
|
element.text = token[1:]
|
||
|
|
else:
|
||
|
|
element.text = token
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
else:
|
||
|
|
element = SubElement(parent, "mi", attrib=attributes)
|
||
|
|
element.text = token
|
||
|
|
_set_font(element, element.tag, font)
|
||
|
|
|
||
|
|
|
||
|
|
def _set_font(element: Element, key: str, font: Optional[dict[str, Optional[str]]]) -> None:
|
||
|
|
if font is None:
|
||
|
|
return
|
||
|
|
_font = font[key]
|
||
|
|
if _font is not None:
|
||
|
|
element.attrib["mathvariant"] = _font
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None: # pragma: no cover
|
||
|
|
import argparse
|
||
|
|
import sys
|
||
|
|
|
||
|
|
parser = argparse.ArgumentParser(description="Pure Python library for LaTeX to MathML conversion")
|
||
|
|
parser.add_argument("-V", "--version", dest="version", action="store_true", required=False, help="Show version")
|
||
|
|
parser.add_argument("-b", "--block", dest="block", action="store_true", required=False, help="Display block")
|
||
|
|
|
||
|
|
required = parser.add_argument_group("required arguments")
|
||
|
|
|
||
|
|
group = required.add_mutually_exclusive_group(required=False)
|
||
|
|
group.add_argument("-t", "--text", dest="text", type=str, required=False, help="Text")
|
||
|
|
group.add_argument("-f", "--file", dest="file", type=str, required=False, help="File")
|
||
|
|
group.add_argument("-s", "--stdin", dest="stdin", action="store_true", required=False, help="Stdin")
|
||
|
|
|
||
|
|
arguments = parser.parse_args()
|
||
|
|
display = "block" if arguments.block else "inline"
|
||
|
|
|
||
|
|
if arguments.version:
|
||
|
|
import latex2mathml
|
||
|
|
|
||
|
|
print("latex2mathml", latex2mathml.__version__)
|
||
|
|
elif arguments.text:
|
||
|
|
print(convert(arguments.text, display=display))
|
||
|
|
elif arguments.file:
|
||
|
|
with open(arguments.file) as f:
|
||
|
|
print(convert(f.read(), display=display))
|
||
|
|
elif arguments.stdin:
|
||
|
|
print(convert(sys.stdin.read(), display=display))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__": # pragma: no cover
|
||
|
|
main()
|