def _convert_command(element, elements, index, iterable, parent): _get_prefix_element(element, parent) params, tag, attributes = COMMANDS[element] new_parent = eTree.SubElement(parent, tag, **attributes) alignment = None if element in MATRICES and (element.endswith('*') or element == r'\array'): index += 1 alignment = elements[index] next(iterable) for j in range(params): index += 1 param = elements[index] if element == r'\left' or element == r'\right': symbol = convert_symbol(param) new_parent.text = param if symbol is None else '&#x{};'.format( symbol) elif element == r'\array': _convert_array_content(param, new_parent, alignment) elif element in MATRICES: _convert_matrix_content(param, new_parent, alignment) else: if isinstance(param, list): _parent = eTree.SubElement(new_parent, 'mrow') _classify_subgroup(param, _parent) else: _classify(param, new_parent) _get_postfix_element(element, parent) if element == r'\overline': mo = eTree.SubElement(new_parent, 'mo', stretchy='true') mo.text = '¯' elif element == r'\underline': mo = eTree.SubElement(new_parent, 'mo', stretchy='true') mo.text = '̲' [next(iterable) for _ in range(params)]
def _classify(_element, parent): symbol = convert_symbol(_element) if re.match(r'\d+(.\d+)?', _element): mn = eTree.SubElement(parent, 'mn') mn.text = _element elif _element in '+-*/()=': mo = eTree.SubElement(parent, 'mo') mo.text = _element if symbol is None else '&#x{};'.format(symbol) 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)): mo = eTree.SubElement(parent, 'mo') mo.text = '&#x{};'.format(symbol) elif _element.startswith('\\'): if symbol: mi = eTree.SubElement(parent, 'mi') mi.text = '&#x{};'.format(symbol) else: e = _element.lstrip('\\') mi = eTree.SubElement(parent, 'mi') mi.text = e else: mi = eTree.SubElement(parent, 'mi') mi.text = _element
def _convert_command( element: str, elements: List[Any], index: int, iterable: Iterator[int], parent: Element, ): _get_prefix_element(element, parent) if element == r"\substack": parent = SubElement(parent, "mstyle", scriptlevel="1") params, tag, attributes = COMMANDS[element] if len(elements) - 1 < params: mo = SubElement(parent, "mo") mo.text = element[1:] return new_parent = SubElement(parent, tag, attributes) alignment = "" if element in MATRICES and (element.endswith("*") or element == r"\array"): index += 1 alignment = elements[index] next(iterable) if element in (r"\lim", r"\inf", r"\sup", r"\max", r"\min"): limit = SubElement(new_parent, "mo") limit.text = element[1:] for j in range(params): index += 1 param = elements[index] if element == "_" and index == 1 and param == r"\sum": new_parent.tag = "munder" _classify(param, new_parent) elif element == r"\left" or element == r"\right": if param == ".": pass else: symbol = convert_symbol(param) new_parent.text = param if symbol is None else "&#x{};".format( symbol) elif element == r"\array": _convert_array_content(param, new_parent, alignment) elif element in MATRICES: _convert_matrix_content(param, new_parent, alignment, element == r"\substack") else: if isinstance(param, list): _parent = SubElement(new_parent, "mrow") _classify_subgroup(param, _parent) else: _classify(param, new_parent) _get_postfix_element(element, parent) if element in (r"\overline", r"\bar"): mo = SubElement(new_parent, "mo", stretchy="true") mo.text = "¯" elif element == r"\underline": mo = SubElement(new_parent, "mo", stretchy="true") mo.text = "̲" elif element in (r"\overrightarrow", r"\vec"): mo = SubElement(new_parent, "mo", stretchy="true") mo.text = "→" [next(iterable) for _ in range(params)]
def _classify(_element: str, parent: Element, is_math_mode: bool = False) -> None: symbol = convert_symbol(_element) if re.match(r"\d+(.\d+)?", _element): mn = SubElement(parent, "mn") mn.text = _element elif len(_element) and _element in "<>&": mo = SubElement(parent, "mo") mo.text = {"<": "<", ">": ">", "&": "&"}[_element] elif len(_element) and _element in "+-*/()=": mo = SubElement(parent, "mo") mo.text = _element if symbol is None else "&#x{};".format(symbol) if _element in "()": mo.attrib["stretchy"] = "false" 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 == "."): mo = SubElement(parent, "mo") mo.text = "&#x{};".format(symbol) elif _element.startswith("\\"): tag = SubElement(parent, "mo" if is_math_mode else "mi") if symbol: tag.text = "&#x{};".format(symbol) elif _element in ( r"\log", r"\ln", r"\tan", r"\sec", r"\cos", r"\sin", r"\cot", r"\csc", ): tag.text = _element[1:] else: tag.text = _element else: tag = SubElement(parent, "mo" if is_math_mode else "mi") tag.text = _element
def tokenize(latex_string: str, skip_comments: bool = True) -> Iterator[str]: """ Converts Latex string into tokens. :param latex_string: Latex string. :param skip_comments: Flag to skip comments (default=True). """ for match in PATTERN.finditer(latex_string): tokens = tuple(filter(lambda x: x is not None, match.groups())) if tokens[0].startswith(commands.MATH): full_math = "".join(tokens) symbol = convert_symbol(full_math) if symbol: yield f"&#x{symbol};" continue for captured in tokens: if skip_comments and captured.startswith("%"): break if captured.endswith(UNITS): yield captured.replace(" ", "") continue yield captured
def test_convert_symbol(latex: str, expected: str) -> None: assert convert_symbol(latex) == expected
def tokenize(data) -> Iterator[str]: iterable = iter(data) buffer = "" while True: try: char = next(iterable) if char == "\\": if buffer == "\\": yield buffer + char buffer = "" continue elif len(buffer): yield buffer buffer = char try: buffer += next(iterable) if buffer in (r"\\", r"\[", r"\]"): yield buffer buffer = "" except StopIteration: break elif char.isalpha(): if len(buffer): if buffer.endswith("}"): yield buffer yield char buffer = "" elif buffer.startswith("\\"): buffer += char else: yield char elif char.isdigit(): if len(buffer): yield buffer buffer = char while True: try: char = next(iterable) except StopIteration: break if char.isspace(): yield buffer buffer = "" break elif char.isdigit() or char == ".": buffer += char else: if buffer.endswith("."): yield buffer[:-1] yield buffer[-1] else: yield buffer buffer = "" if char == "\\": buffer = char else: yield char break elif char.isspace(): if len(buffer): yield buffer buffer = "" elif char in "{}*": # FIXME: Anything that starts with '\math' passes. There is a huge list of math symbols in # unimathsymbols.txt and hard-coding all of them is inefficient. if (buffer.startswith(r"\begin") or buffer.startswith(r"\end") or buffer.startswith(r"\math")): if buffer.endswith("}"): yield buffer yield char buffer = "" continue elif buffer.startswith(r"\math") and char == "}": symbol = convert_symbol(buffer + char) if symbol: yield "&#x{};".format(symbol) buffer = "" continue buffer += char else: if len(buffer): yield buffer buffer = "" yield char else: if len(buffer): if buffer.startswith(r"\math"): yield buffer[:-1] yield buffer[-1] else: yield buffer buffer = "" if len(char): yield char except StopIteration: break if len(buffer): yield buffer
def test_convert_symbol(name: str, latex: str, expected: str): assert convert_symbol(latex) == expected
def _convert_and_append_operator(symbol, parent): symbol = convert_symbol(symbol) mo = eTree.SubElement(parent, 'mo') mo.text = '&#x{};'.format(symbol)
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: 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 _convert_and_append_operator(symbol: str, parent: Element) -> None: converted = convert_symbol(symbol) mo = SubElement(parent, "mo") mo.text = "&#x{};".format(converted)
def test_operator_plus(self): self.assertEqual('0002B', symbols_parser.convert_symbol('+'))
def test_alias_command(self): self.assertEqual('02192', symbols_parser.convert_symbol(r'\to'))
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 _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 test_operator_plus(): assert '0002B' == convert_symbol('+')
def test_alias_command(): assert '02192' == convert_symbol(r'\to')
def _walk(tokens: Iterator[str], terminator: str = None, limit: int = 0) -> 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: 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 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) 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 ): node = Node(token=commands.SUPERSCRIPT, children=(previous.children[0], Node(token=commands.DPRIME))) 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