math2tex 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- math2tex/__init__.py +84 -0
- math2tex/ast_nodes.py +205 -0
- math2tex/cli.py +60 -0
- math2tex/config.py +213 -0
- math2tex/formatter.py +504 -0
- math2tex/lexer.py +160 -0
- math2tex/macros.py +408 -0
- math2tex/parser.py +967 -0
- math2tex/tests_legacy.py +195 -0
- math2tex-1.0.0.dist-info/METADATA +135 -0
- math2tex-1.0.0.dist-info/RECORD +14 -0
- math2tex-1.0.0.dist-info/WHEEL +4 -0
- math2tex-1.0.0.dist-info/entry_points.txt +2 -0
- math2tex-1.0.0.dist-info/licenses/LICENSE +21 -0
math2tex/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math2tex — A professional plaintext-to-LaTeX mathematical expression engine.
|
|
3
|
+
|
|
4
|
+
Converts informal math notation into high-quality, publication-ready LaTeX.
|
|
5
|
+
Designed for use with the LaTeX ``physics`` package.
|
|
6
|
+
|
|
7
|
+
Public API
|
|
8
|
+
----------
|
|
9
|
+
.. autofunction:: math2tex
|
|
10
|
+
.. autofunction:: math2tex_multiline
|
|
11
|
+
.. autofunction:: parse_macro_def
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from .config import Config
|
|
17
|
+
from .lexer import ParseError, _preprocess, tokenize
|
|
18
|
+
from .parser import Parser
|
|
19
|
+
from .formatter import LatexFormatter
|
|
20
|
+
from .macros import parse_macro_def, Macro, substitute
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'math2tex',
|
|
24
|
+
'math2tex_multiline',
|
|
25
|
+
'parse_macro_def',
|
|
26
|
+
'Config',
|
|
27
|
+
'ParseError',
|
|
28
|
+
'Macro',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def math2tex(expression: str,
|
|
33
|
+
macro_env: Optional[dict] = None) -> str:
|
|
34
|
+
try:
|
|
35
|
+
if macro_env is None:
|
|
36
|
+
macro_env = {}
|
|
37
|
+
|
|
38
|
+
# Expand zero-argument macros before lexing
|
|
39
|
+
macro = macro_env.get(expression.strip())
|
|
40
|
+
if macro is not None and not macro.args:
|
|
41
|
+
formatter = LatexFormatter({})
|
|
42
|
+
return formatter.format(macro.body, is_root=True)
|
|
43
|
+
|
|
44
|
+
preprocessed, text_map = _preprocess(expression)
|
|
45
|
+
tokens = tokenize(preprocessed)
|
|
46
|
+
parser = Parser(tokens, text_map=text_map, macro_env=macro_env)
|
|
47
|
+
ast = parser.parse()
|
|
48
|
+
formatter = LatexFormatter(text_map)
|
|
49
|
+
return formatter.format(ast, is_root=True)
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return f'% Parse Error: {e}'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def math2tex_multiline(expressions: List[str],
|
|
56
|
+
env: str = None,
|
|
57
|
+
macro_env: Optional[dict] = None) -> str:
|
|
58
|
+
if env is None:
|
|
59
|
+
env = Config.DEFAULT_ENV
|
|
60
|
+
if macro_env is None:
|
|
61
|
+
macro_env = {}
|
|
62
|
+
|
|
63
|
+
align_mode = (env == 'align*')
|
|
64
|
+
|
|
65
|
+
formatted_lines: List[str] = []
|
|
66
|
+
for expr in expressions:
|
|
67
|
+
if not expr.strip():
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
preprocessed, text_map = _preprocess(expr)
|
|
71
|
+
tokens = tokenize(preprocessed)
|
|
72
|
+
parser = Parser(tokens, text_map=text_map, macro_env=macro_env)
|
|
73
|
+
ast = parser.parse()
|
|
74
|
+
formatter = LatexFormatter(text_map, align_mode=align_mode)
|
|
75
|
+
formatted = formatter.format(ast, is_root=True)
|
|
76
|
+
formatted_lines.append(formatted)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
formatted_lines.append(f'% Parse Error: {e}')
|
|
79
|
+
|
|
80
|
+
if env == 'none':
|
|
81
|
+
return '\n'.join(formatted_lines)
|
|
82
|
+
|
|
83
|
+
content = ' \\\\\n'.join(formatted_lines)
|
|
84
|
+
return f'\\begin{{{env}}}\n{content}\n\\end{{{env}}}'
|
math2tex/ast_nodes.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math2tex.ast_nodes — Abstract Syntax Tree node definitions
|
|
3
|
+
|
|
4
|
+
Every AST node is a frozen dataclass carrying a `precedence` property
|
|
5
|
+
used by the formatter to decide where to insert parentheses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from .config import Prec, _REL_OPS, _ADD_OPS, _MULT_OPS
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# Base class
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
class Node:
|
|
18
|
+
@property
|
|
19
|
+
def precedence(self) -> int:
|
|
20
|
+
return Prec.ATOM
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Atoms
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Num(Node):
|
|
28
|
+
val: str
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Var(Node):
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TextLiteral(Node):
|
|
36
|
+
val: str
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Group(Node):
|
|
40
|
+
expr: Node
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Binary / Unary Operations
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class BinOp(Node):
|
|
48
|
+
op: str
|
|
49
|
+
left: Node
|
|
50
|
+
right: Node
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def precedence(self) -> int:
|
|
54
|
+
if self.op in _REL_OPS:
|
|
55
|
+
return Prec.REL
|
|
56
|
+
if self.op == 'choose':
|
|
57
|
+
return Prec.CHOOSE
|
|
58
|
+
if self.op in _ADD_OPS:
|
|
59
|
+
return Prec.ADD
|
|
60
|
+
if self.op in _MULT_OPS:
|
|
61
|
+
return Prec.MULT
|
|
62
|
+
return Prec.LOWEST
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class UnOp(Node):
|
|
66
|
+
op: str
|
|
67
|
+
expr: Node
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def precedence(self) -> int:
|
|
71
|
+
return Prec.UNARY
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Fraction / Power / Subscript
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class Fraction(Node):
|
|
79
|
+
num: Node
|
|
80
|
+
den: Node
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class Power(Node):
|
|
84
|
+
base: Node
|
|
85
|
+
exp: Node
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def precedence(self) -> int:
|
|
89
|
+
return Prec.POW
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Subscript(Node):
|
|
93
|
+
base: Node
|
|
94
|
+
sub: Node
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def precedence(self) -> int:
|
|
98
|
+
return Prec.POW
|
|
99
|
+
|
|
100
|
+
# =============================================================================
|
|
101
|
+
# Functions
|
|
102
|
+
# =============================================================================
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class Func(Node):
|
|
106
|
+
name: str
|
|
107
|
+
args: list
|
|
108
|
+
sub: Optional[Node] = None
|
|
109
|
+
power: Optional[Node] = None
|
|
110
|
+
primes: int = 0
|
|
111
|
+
call_args: Optional[list] = None
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# Postfix Operators
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class Factorial(Node):
|
|
119
|
+
expr: Node
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def precedence(self) -> int:
|
|
123
|
+
return Prec.POSTFIX
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class Prime(Node):
|
|
128
|
+
expr: Node
|
|
129
|
+
order: int
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def precedence(self) -> int:
|
|
133
|
+
return Prec.POSTFIX
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Calculus
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class Derivative(Node):
|
|
141
|
+
expr: Node
|
|
142
|
+
var: str
|
|
143
|
+
order: Optional[Node]
|
|
144
|
+
is_partial: bool
|
|
145
|
+
var2: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class Integral(Node):
|
|
150
|
+
expr: Node
|
|
151
|
+
lower: Optional[Node]
|
|
152
|
+
upper: Optional[Node]
|
|
153
|
+
kind: str
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class BigOp(Node):
|
|
157
|
+
"""BigOp class representing bigop."""
|
|
158
|
+
op_name: str # 'sum', 'prod', 'lim', etc.
|
|
159
|
+
lower: Optional[Node]
|
|
160
|
+
upper: Optional[Node]
|
|
161
|
+
expr: Node
|
|
162
|
+
|
|
163
|
+
# =============================================================================
|
|
164
|
+
# Delimiters & Structures
|
|
165
|
+
# =============================================================================
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class Abs(Node):
|
|
169
|
+
expr: Node
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class Norm(Node):
|
|
174
|
+
expr: Node
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class InnerProduct(Node):
|
|
179
|
+
left: Node
|
|
180
|
+
right: Node
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class SetBuilder(Node):
|
|
185
|
+
expr: Node
|
|
186
|
+
condition: Node
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class MatrixNode(Node):
|
|
191
|
+
rows: list
|
|
192
|
+
kind: str = 'pmatrix'
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class IntervalNode(Node):
|
|
197
|
+
left_delim: str
|
|
198
|
+
left_expr: Node
|
|
199
|
+
right_expr: Node
|
|
200
|
+
right_delim: str
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class PiecewiseNode(Node):
|
|
205
|
+
conditions: list
|
math2tex/cli.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math2tex.cli — Interactive command-line interface.
|
|
3
|
+
|
|
4
|
+
Provides an interactive REPL for converting plaintext math to LaTeX,
|
|
5
|
+
with support for multi-line input and macro definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import math2tex, math2tex_multiline, parse_macro_def
|
|
11
|
+
from .config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
"""Run the interactive conversion loop."""
|
|
16
|
+
print(f"\nInteractive math2tex converter.")
|
|
17
|
+
print(f"Type '{Config.MULTI_LINE_END_KEYWORD}' on a new line "
|
|
18
|
+
f"to process multi-line input.")
|
|
19
|
+
print(f"Type 'quit' or 'exit' to terminate.\n")
|
|
20
|
+
|
|
21
|
+
while True:
|
|
22
|
+
lines = []
|
|
23
|
+
while True:
|
|
24
|
+
try:
|
|
25
|
+
line = input("> " if not lines else "... ").strip()
|
|
26
|
+
except EOFError:
|
|
27
|
+
sys.exit(0)
|
|
28
|
+
|
|
29
|
+
if not lines and line.lower() in {'quit', 'exit'}:
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
if line.upper() == Config.MULTI_LINE_END_KEYWORD:
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
if line:
|
|
36
|
+
lines.append(line)
|
|
37
|
+
|
|
38
|
+
if not lines:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if len(lines) == 1:
|
|
42
|
+
res = math2tex(lines[0])
|
|
43
|
+
print(f"$${res}$$")
|
|
44
|
+
else:
|
|
45
|
+
env_prompt = input(
|
|
46
|
+
f"Multiple lines detected. Output in (1) gather* "
|
|
47
|
+
f"or (2) align* environment? "
|
|
48
|
+
f"[Default: {Config.DEFAULT_ENV}]: "
|
|
49
|
+
).strip()
|
|
50
|
+
if env_prompt == '1' or env_prompt.lower() == 'gather*':
|
|
51
|
+
env = 'gather*'
|
|
52
|
+
elif env_prompt == '2' or env_prompt.lower() == 'align*':
|
|
53
|
+
env = 'align*'
|
|
54
|
+
else:
|
|
55
|
+
env = Config.DEFAULT_ENV
|
|
56
|
+
|
|
57
|
+
result = math2tex_multiline(lines, env)
|
|
58
|
+
print(f"{result}")
|
|
59
|
+
|
|
60
|
+
main()
|
math2tex/config.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math2tex.config — Constants, maps, and global configuration.
|
|
3
|
+
|
|
4
|
+
All token dictionaries, operator sets, and structural configuration
|
|
5
|
+
values are centralised here to provide a single source of truth across
|
|
6
|
+
all compiler modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# Global Configuration
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
class Config:
|
|
14
|
+
DEFAULT_ENV = 'gather*'
|
|
15
|
+
AUTO_ALIGN_EQUALS = True
|
|
16
|
+
ADD_DX_TO_INTEGRALS = True
|
|
17
|
+
MULTI_LINE_END_KEYWORD = 'DONE'
|
|
18
|
+
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# Unicode Translation Map
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
UNICODE_MAP = {
|
|
24
|
+
'α': 'alpha', 'β': 'beta', 'γ': 'gamma', 'δ': 'delta', 'ε': 'epsilon',
|
|
25
|
+
'varepsilon': 'varepsilon',
|
|
26
|
+
'ζ': 'zeta', 'η': 'eta', 'θ': 'theta', 'ι': 'iota', 'κ': 'kappa',
|
|
27
|
+
'λ': 'lambda', 'μ': 'mu', 'ν': 'nu', 'ξ': 'xi', 'π': 'pi',
|
|
28
|
+
'ρ': 'rho', 'σ': 'sigma', 'τ': 'tau', 'υ': 'upsilon', 'φ': 'phi',
|
|
29
|
+
'χ': 'chi', 'ψ': 'psi', 'ω': 'omega',
|
|
30
|
+
'Γ': 'Gamma', 'Δ': 'Delta', 'Θ': 'Theta', 'Λ': 'Lambda',
|
|
31
|
+
'Ξ': 'Xi', 'Π': 'Pi', 'Σ': 'Sigma', 'Υ': 'Upsilon',
|
|
32
|
+
'Φ': 'Phi', 'Ψ': 'Psi', 'Ω': 'Omega',
|
|
33
|
+
'∫': 'integral', '∬': 'iint', '∭': 'iiint', '∮': 'oint', '∯': 'oiint',
|
|
34
|
+
'∇': 'nabla', '∂': 'partial', '∞': 'oo',
|
|
35
|
+
'≈': 'approx', '≡': 'equiv', '∝': 'propto',
|
|
36
|
+
'≠': '!=',
|
|
37
|
+
'≤': '<=', '≥': '>=', '∈': 'in', '∉': 'notin', '⊂': 'subset',
|
|
38
|
+
'⊃': 'supset', '⊆': 'subseteq', '⊇': 'supseteq',
|
|
39
|
+
'∅': 'emptyset',
|
|
40
|
+
'∪': 'cup', '∩': 'cap', '×': 'times', '±': 'pm', '∓': 'mp',
|
|
41
|
+
'→': '->', '⇒': 'implies', '⇔': 'iff', '↦': 'mapsto',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Function & Operator Vocabularies
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
SPLITTABLE_FUNCS = tuple(sorted([
|
|
49
|
+
'sin', 'cos', 'tan', 'cot', 'sec', 'csc',
|
|
50
|
+
'arcsin', 'arccos', 'arctan', 'arccot', 'arcsec', 'arccsc',
|
|
51
|
+
'sinh', 'cosh', 'tanh', 'coth',
|
|
52
|
+
'ln', 'log', 'exp', 'det', 'dim', 'ker', 'deg', 'arg', 'gcd',
|
|
53
|
+
'min', 'max',
|
|
54
|
+
], key=lambda x: (-len(x), x)))
|
|
55
|
+
|
|
56
|
+
MATRIX_FUNCS = {
|
|
57
|
+
'pmat': 'pmatrix', 'bmat': 'bmatrix', 'vmat': 'vmatrix',
|
|
58
|
+
'Vmat': 'Vmatrix', 'Bmat': 'Bmatrix',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
GREEK_LETTERS = {
|
|
62
|
+
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'varepsilon', 'zeta',
|
|
63
|
+
'eta', 'theta', 'vartheta', 'iota', 'kappa', 'lambda', 'mu', 'nu',
|
|
64
|
+
'xi', 'pi', 'varpi', 'rho', 'varrho', 'sigma', 'varsigma', 'tau',
|
|
65
|
+
'upsilon', 'phi', 'varphi', 'chi', 'psi', 'omega',
|
|
66
|
+
'Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', 'Pi', 'Sigma', 'Upsilon',
|
|
67
|
+
'Phi', 'Psi', 'Omega',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
SPECIAL_CONSTANTS = {
|
|
71
|
+
'oo': r'\infty', 'infinity': r'\infty', 'infty': r'\infty', 'inf': r'\infty',
|
|
72
|
+
'Re': r'\Re', 'Im': r'\Im',
|
|
73
|
+
'nabla': r'\nabla', 'grad': r'\grad', 'curl': r'\curl',
|
|
74
|
+
'laplacian': r'\laplacian',
|
|
75
|
+
'hbar': r'\hbar', 'ell': r'\ell', 'aleph': r'\aleph',
|
|
76
|
+
'emptyset': r'\emptyset', 'varnothing': r'\varnothing',
|
|
77
|
+
'imath': r'\imath', 'jmath': r'\jmath',
|
|
78
|
+
'forall': r'\forall', 'exists': r'\exists', 'nexists': r'\nexists',
|
|
79
|
+
'therefore': r'\therefore', 'because': r'\because',
|
|
80
|
+
'cdots': r'\cdots', 'ldots': r'\ldots', 'vdots': r'\vdots',
|
|
81
|
+
'ddots': r'\ddots',
|
|
82
|
+
'QED': r'\blacksquare', 'qed': r'\blacksquare',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Operator Vocabularies (word-form)
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
# Word-form relational operators (parsed at relational precedence level)
|
|
90
|
+
WORD_RELATIONS = {
|
|
91
|
+
'in': r'\in', 'notin': r'\notin', 'ni': r'\ni',
|
|
92
|
+
'subset': r'\subset', 'supset': r'\supset',
|
|
93
|
+
'subseteq': r'\subseteq', 'supseteq': r'\supseteq',
|
|
94
|
+
'approx': r'\approx', 'equiv': r'\equiv',
|
|
95
|
+
'propto': r'\propto', 'sim': r'\sim', 'cong': r'\cong',
|
|
96
|
+
'perp': r'\perp', 'neq': r'\neq', 'ne': r'\neq',
|
|
97
|
+
'implies': r'\implies', 'iff': r'\iff', 'mapsto': r'\mapsto',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Word-form multiplicative operators
|
|
101
|
+
WORD_MULT_OPS = {
|
|
102
|
+
'times': r'\times', 'cross': r'\times',
|
|
103
|
+
'cdot': r'\cdot',
|
|
104
|
+
'cup': r'\cup', 'cap': r'\cap',
|
|
105
|
+
'union': r'\cup', 'intersect': r'\cap',
|
|
106
|
+
'setminus': r'\setminus',
|
|
107
|
+
'oplus': r'\oplus', 'otimes': r'\otimes',
|
|
108
|
+
'wedge': r'\wedge', 'vee': r'\vee',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Word-form additive operators
|
|
112
|
+
WORD_ADD_OPS = {
|
|
113
|
+
'pm': r'\pm', 'mp': r'\mp',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# =============================================================================
|
|
117
|
+
# Function Classification Sets
|
|
118
|
+
# =============================================================================
|
|
119
|
+
|
|
120
|
+
BUILTIN_LATEX_FUNCS = {
|
|
121
|
+
'sin', 'cos', 'tan', 'cot', 'sec', 'csc',
|
|
122
|
+
'arcsin', 'arccos', 'arctan', 'arccot', 'arcsec', 'arccsc',
|
|
123
|
+
'sinh', 'cosh', 'tanh', 'coth',
|
|
124
|
+
'ln', 'log', 'exp', 'min', 'max',
|
|
125
|
+
'det', 'dim', 'ker', 'deg', 'arg', 'gcd',
|
|
126
|
+
'Re', 'Im',
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ACCENT_FUNCTIONS = {
|
|
130
|
+
'hat': 'hat', 'bar': 'bar', 'vec': 'vec', 'tilde': 'tilde',
|
|
131
|
+
'dot': 'dot', 'ddot': 'ddot', 'dddot': 'dddot',
|
|
132
|
+
'overline': 'overline', 'underline': 'underline',
|
|
133
|
+
'widehat': 'widehat', 'widetilde': 'widetilde',
|
|
134
|
+
'check': 'check', 'breve': 'breve', 'acute': 'acute',
|
|
135
|
+
'grave': 'grave', 'mathring': 'mathring',
|
|
136
|
+
'overbrace': 'overbrace', 'underbrace': 'underbrace',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
FONT_FUNCTIONS = {
|
|
140
|
+
'mathbf': 'mathbf', 'mathrm': 'mathrm', 'mathit': 'mathit',
|
|
141
|
+
'mathcal': 'mathcal', 'mathbb': 'mathbb', 'mathfrak': 'mathfrak',
|
|
142
|
+
'mathscr': 'mathscr', 'boldsymbol': 'boldsymbol', 'bm': 'bm',
|
|
143
|
+
'operatorname': 'operatorname',
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
BIG_OPS = {
|
|
147
|
+
'sum', 'prod', 'lim', 'inf', 'sup', 'limsup', 'liminf',
|
|
148
|
+
'integral', 'integrate', 'int', 'iint', 'iiint', 'oint', 'oiint',
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
SPECIAL_FUNC_NAMES = (
|
|
152
|
+
{'sqrt', 'root', 'floor', 'ceil', 'exp', 'abs', 'norm',
|
|
153
|
+
'text', 'binom', 'boxed', 'cancel'}
|
|
154
|
+
| set(ACCENT_FUNCTIONS) | set(FONT_FUNCTIONS)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
ALL_FUNC_NAMES = BUILTIN_LATEX_FUNCS | SPECIAL_FUNC_NAMES
|
|
158
|
+
|
|
159
|
+
IMPLICIT_MULT_STOP = (
|
|
160
|
+
{'if', 'otherwise', 'else', 'choose', 'from', 'to', 'of', 'as'}
|
|
161
|
+
| set(WORD_RELATIONS) | set(WORD_MULT_OPS) | set(WORD_ADD_OPS)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# =============================================================================
|
|
165
|
+
# Precedence Levels
|
|
166
|
+
# =============================================================================
|
|
167
|
+
|
|
168
|
+
class Prec:
|
|
169
|
+
"""Precedence levels for parsing and formatting."""
|
|
170
|
+
LOWEST = 0
|
|
171
|
+
REL = 1 # =, <, >, <=, >=, ->, in, implies, etc.
|
|
172
|
+
CHOOSE = 2 # choose (binomial)
|
|
173
|
+
ADD = 3 # +, -, pm, mp
|
|
174
|
+
MULT = 4 # *, /, implicit multiplication, times, cdot
|
|
175
|
+
UNARY = 6 # -x, +x, neg
|
|
176
|
+
POW = 5 # ^, _
|
|
177
|
+
POSTFIX = 7 # !, '
|
|
178
|
+
ATOM = 8 # numbers, vars, fractions, grouped
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Operator category sets for BinOp precedence
|
|
182
|
+
_REL_OPS = (
|
|
183
|
+
{':', '=', '<', '>', '<=', '>=', '==', '->', '!=', '~='}
|
|
184
|
+
| set(WORD_RELATIONS)
|
|
185
|
+
)
|
|
186
|
+
_ADD_OPS = {'+', '-', '+-', '-+'} | set(WORD_ADD_OPS)
|
|
187
|
+
_MULT_OPS = {'*', ' '} | set(WORD_MULT_OPS)
|
|
188
|
+
|
|
189
|
+
# =============================================================================
|
|
190
|
+
# Parser Limits
|
|
191
|
+
# =============================================================================
|
|
192
|
+
|
|
193
|
+
MAX_DEPTH = 200
|
|
194
|
+
|
|
195
|
+
# =============================================================================
|
|
196
|
+
# Utility Functions
|
|
197
|
+
# =============================================================================
|
|
198
|
+
|
|
199
|
+
def levenshtein_distance(s1: str, s2: str) -> int:
|
|
200
|
+
if len(s1) < len(s2):
|
|
201
|
+
return levenshtein_distance(s2, s1)
|
|
202
|
+
if len(s2) == 0:
|
|
203
|
+
return len(s1)
|
|
204
|
+
previous_row = range(len(s2) + 1)
|
|
205
|
+
for i, c1 in enumerate(s1):
|
|
206
|
+
current_row = [i + 1]
|
|
207
|
+
for j, c2 in enumerate(s2):
|
|
208
|
+
insertions = previous_row[j + 1] + 1
|
|
209
|
+
deletions = current_row[j] + 1
|
|
210
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
211
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
212
|
+
previous_row = current_row
|
|
213
|
+
return previous_row[-1]
|