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 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]