asciimath2 0.1.0__tar.gz
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.
- asciimath2-0.1.0/LICENSE +21 -0
- asciimath2-0.1.0/PKG-INFO +151 -0
- asciimath2-0.1.0/README.md +124 -0
- asciimath2-0.1.0/pyproject.toml +49 -0
- asciimath2-0.1.0/pyproject.toml.orig +45 -0
- asciimath2-0.1.0/src/asciimath2/__init__.py +10 -0
- asciimath2-0.1.0/src/asciimath2/cli.py +28 -0
- asciimath2-0.1.0/src/asciimath2/config.py +46 -0
- asciimath2-0.1.0/src/asciimath2/errors.py +15 -0
- asciimath2-0.1.0/src/asciimath2/latex_context.py +50 -0
- asciimath2-0.1.0/src/asciimath2/py.typed +0 -0
- asciimath2-0.1.0/src/asciimath2/render.py +593 -0
- asciimath2-0.1.0/src/asciimath2/symbols.py +214 -0
asciimath2-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asciimath2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LaTeX -> AsciiMath2 conversion (AsciiMath2 = asciimath.org + the asciimath-parser extensions, see SPEC.md)
|
|
5
|
+
Keywords: latex,asciimath,asciimath2,math,converter,markup
|
|
6
|
+
Author: Alex Lee
|
|
7
|
+
Author-email: Alex Lee <iasandcb@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: pylatexenc>=2.11
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Project-URL: Homepage, https://github.com/iasandcb/asciimath2
|
|
23
|
+
Project-URL: Repository, https://github.com/iasandcb/asciimath2
|
|
24
|
+
Project-URL: Issues, https://github.com/iasandcb/asciimath2/issues
|
|
25
|
+
Project-URL: Specification, https://github.com/iasandcb/asciimath2/blob/main/SPEC.md
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# asciimath2
|
|
29
|
+
|
|
30
|
+
**AsciiMath2** is asciimath.org's grammar extended with everything
|
|
31
|
+
[`asciimath-parser`](https://github.com/widcardw/asciimath-parser) already
|
|
32
|
+
implements (matrices, aligned multi-line equations, colors, fonts, accents,
|
|
33
|
+
...), plus two more contributions on top of that:
|
|
34
|
+
[render-time configurability](https://github.com/widcardw/asciimath-parser/pull/22)
|
|
35
|
+
and a [symbol-table isolation contract](https://github.com/widcardw/asciimath-parser/pull/21).
|
|
36
|
+
The full grammar is written down in **[SPEC.md](https://github.com/iasandcb/asciimath2/blob/main/SPEC.md)** — read that first.
|
|
37
|
+
|
|
38
|
+
This directory is the standard plus one from-scratch implementation of one
|
|
39
|
+
direction through it:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
LaTeX --[this package]--> AsciiMath2 --[asciimath-parser]--> LaTeX
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`asciimath-parser` already goes AsciiMath2 → LaTeX. This package goes the
|
|
46
|
+
other way, which nothing implemented before. Together they round-trip.
|
|
47
|
+
|
|
48
|
+
## Install & use
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv sync # or: pip install -e .
|
|
52
|
+
uv run asciimath2 '\frac{1}{2} + \sqrt{x^2+1}'
|
|
53
|
+
# 1/2 + sqrt(x^2 + 1)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from asciimath2 import convert, Config, UnsupportedLatexError
|
|
58
|
+
|
|
59
|
+
convert(r"\sum_{i=1}^n i^2")
|
|
60
|
+
# 'sum_(i = 1)^n i^2'
|
|
61
|
+
|
|
62
|
+
convert(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}")
|
|
63
|
+
# '(a, b; c, d)'
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
convert(r"\begin{tikzpicture}...\end{tikzpicture}")
|
|
67
|
+
except UnsupportedLatexError:
|
|
68
|
+
... # fall back to keeping the original LaTeX -- see "On failure" below
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`Config` mirrors the three render options from PR #22
|
|
72
|
+
(`multiline_env`, `single_newline_break`, `bar_as_mid` — see SPEC.md §2.2);
|
|
73
|
+
only `single_newline_break` changes what this package emits.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
`src/asciimath2/`:
|
|
78
|
+
|
|
79
|
+
- `latex_context.py` — extends `pylatexenc`'s macro database with argument
|
|
80
|
+
specs for macros it doesn't know the arity of (`\binom`, `\overset`,
|
|
81
|
+
`\operatorname`, ...), so pylatexenc attaches their `{...}` groups as real
|
|
82
|
+
arguments instead of leaving them as unrelated sibling nodes.
|
|
83
|
+
- `symbols.py` — the LaTeX-command → AsciiMath2-token tables. Ground truth
|
|
84
|
+
is `asciimath-parser`'s own `SYMBOLMAP`
|
|
85
|
+
(`packages/core/src/symbols.ts`), read directly from source, not
|
|
86
|
+
paraphrased from docs.
|
|
87
|
+
- `render.py` — flattens a pylatexenc node tree into a linear token stream,
|
|
88
|
+
then does a single left-to-right pass building AsciiMath2 text. `^`/`_`
|
|
89
|
+
bind to exactly the token before/after them; any compound expression used
|
|
90
|
+
as a required single argument gets wrapped in `(...)`, which AsciiMath2's
|
|
91
|
+
own grammar strips invisibly in exactly those positions — so this
|
|
92
|
+
converter never has to reimplement LaTeX operator precedence itself.
|
|
93
|
+
- `cli.py` — `asciimath2 '<latex>'` / reads stdin.
|
|
94
|
+
|
|
95
|
+
## Scope, honestly
|
|
96
|
+
|
|
97
|
+
This is a broad, tested common subset of LaTeX math — not a general LaTeX
|
|
98
|
+
interpreter (that's not a well-defined target; LaTeX can define arbitrary
|
|
99
|
+
macros). What's covered: arithmetic, fractions/roots, sub/superscripts,
|
|
100
|
+
`\left`/`\right` delimiters (all standard pairs), big operators with limits,
|
|
101
|
+
trig/log/etc. functions, Greek letters, relations/logic/arrows, fonts and
|
|
102
|
+
accents, `\text`, `\binom`/`\operatorname`/`\pmod`/`\color`, and matrix-like
|
|
103
|
+
and aligned-like environments (`pmatrix`/`bmatrix`/`vmatrix`/`Vmatrix`/
|
|
104
|
+
`matrix`/`array`/`cases`, `aligned`/`align`/`gather`/`split`/...).
|
|
105
|
+
|
|
106
|
+
Two deliberate choices about what happens outside that:
|
|
107
|
+
|
|
108
|
+
- **`tex(...)` escape hatch** — an unrecognized *bare* macro with no
|
|
109
|
+
arguments (e.g. `\digamma`) degrades to `tex(\digamma)`, AsciiMath2's own
|
|
110
|
+
raw-LaTeX passthrough (SPEC.md §2.4), rather than failing the whole
|
|
111
|
+
expression.
|
|
112
|
+
- **On failure, raise, don't guess** — anything else unsupported (an unknown
|
|
113
|
+
macro that's ambiguous about whether it takes arguments, an unrecognized
|
|
114
|
+
environment, a `\left` with no matching `\right`, ...) raises
|
|
115
|
+
`UnsupportedLatexError` for the *whole* expression rather than silently
|
|
116
|
+
emitting something plausible-looking but wrong. This is the same contract
|
|
117
|
+
mark-vector's current `backend/app/latex_to_asciimath.py` already expects
|
|
118
|
+
from `py-asciimath` (`None`/exception on failure → keep the original LaTeX
|
|
119
|
+
visible via `\(...\)`/`\[...\]`), so this package is a drop-in candidate
|
|
120
|
+
for that integration point if/when it's wired in — see "Not yet done"
|
|
121
|
+
below.
|
|
122
|
+
|
|
123
|
+
## Integration status
|
|
124
|
+
|
|
125
|
+
Both follow-ups from the initial version of this package are now done:
|
|
126
|
+
|
|
127
|
+
- **Wired into mark-vector's backend.** `backend/app/latex_to_asciimath.py`
|
|
128
|
+
uses this package (added as a local editable dependency in
|
|
129
|
+
`backend/pyproject.toml`) instead of `py-asciimath` — which is the whole
|
|
130
|
+
reason this package exists (`py-asciimath` couldn't do matrices, `\\`,
|
|
131
|
+
and other constructs this package handles). Multi-row output (aligned
|
|
132
|
+
systems, etc.) is preserved as blank-line-separated rows inside the
|
|
133
|
+
`$$...$$` block it writes back to the document, rather than being
|
|
134
|
+
collapsed to one line, so the row structure survives to the frontend.
|
|
135
|
+
- **Frontend renders AsciiMath2's extensions.** `frontend/static/js/markdown.js`
|
|
136
|
+
imports `asciimath-parser` directly as an ES module from jsDelivr
|
|
137
|
+
(`https://cdn.jsdelivr.net/npm/asciimath-parser@<version>/dist/index.js` —
|
|
138
|
+
it ships no browser-global/UMD build, unlike the `asciimath2tex` package it
|
|
139
|
+
replaced) instead of `asciimath2tex`, which only implemented the base
|
|
140
|
+
asciimath.org grammar. For the AsciiMath dialect, a full `$$...$$` block is
|
|
141
|
+
now handed to the parser in one call rather than being split line-by-line,
|
|
142
|
+
so `asciimath-parser`'s own row/`&`-alignment handling (SPEC.md §2.2) does
|
|
143
|
+
the work instead of the frontend approximating it with a `gathered`
|
|
144
|
+
environment. The LaTeX dialect's per-line `gathered` stacking is
|
|
145
|
+
unchanged, since LaTeX has no equivalent convention.
|
|
146
|
+
|
|
147
|
+
Verified end to end (`asciimath2.convert()` output fed through the actual
|
|
148
|
+
published `asciimath-parser@0.6.11` and, in a real browser via Playwright,
|
|
149
|
+
through KaTeX) for derivatives/limits, matrix multiplication, aligned
|
|
150
|
+
systems of equations, piecewise `cases`, and norms — no parse errors,
|
|
151
|
+
correct LaTeX, correct rendering.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# asciimath2
|
|
2
|
+
|
|
3
|
+
**AsciiMath2** is asciimath.org's grammar extended with everything
|
|
4
|
+
[`asciimath-parser`](https://github.com/widcardw/asciimath-parser) already
|
|
5
|
+
implements (matrices, aligned multi-line equations, colors, fonts, accents,
|
|
6
|
+
...), plus two more contributions on top of that:
|
|
7
|
+
[render-time configurability](https://github.com/widcardw/asciimath-parser/pull/22)
|
|
8
|
+
and a [symbol-table isolation contract](https://github.com/widcardw/asciimath-parser/pull/21).
|
|
9
|
+
The full grammar is written down in **[SPEC.md](https://github.com/iasandcb/asciimath2/blob/main/SPEC.md)** — read that first.
|
|
10
|
+
|
|
11
|
+
This directory is the standard plus one from-scratch implementation of one
|
|
12
|
+
direction through it:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
LaTeX --[this package]--> AsciiMath2 --[asciimath-parser]--> LaTeX
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`asciimath-parser` already goes AsciiMath2 → LaTeX. This package goes the
|
|
19
|
+
other way, which nothing implemented before. Together they round-trip.
|
|
20
|
+
|
|
21
|
+
## Install & use
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv sync # or: pip install -e .
|
|
25
|
+
uv run asciimath2 '\frac{1}{2} + \sqrt{x^2+1}'
|
|
26
|
+
# 1/2 + sqrt(x^2 + 1)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from asciimath2 import convert, Config, UnsupportedLatexError
|
|
31
|
+
|
|
32
|
+
convert(r"\sum_{i=1}^n i^2")
|
|
33
|
+
# 'sum_(i = 1)^n i^2'
|
|
34
|
+
|
|
35
|
+
convert(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}")
|
|
36
|
+
# '(a, b; c, d)'
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
convert(r"\begin{tikzpicture}...\end{tikzpicture}")
|
|
40
|
+
except UnsupportedLatexError:
|
|
41
|
+
... # fall back to keeping the original LaTeX -- see "On failure" below
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`Config` mirrors the three render options from PR #22
|
|
45
|
+
(`multiline_env`, `single_newline_break`, `bar_as_mid` — see SPEC.md §2.2);
|
|
46
|
+
only `single_newline_break` changes what this package emits.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
`src/asciimath2/`:
|
|
51
|
+
|
|
52
|
+
- `latex_context.py` — extends `pylatexenc`'s macro database with argument
|
|
53
|
+
specs for macros it doesn't know the arity of (`\binom`, `\overset`,
|
|
54
|
+
`\operatorname`, ...), so pylatexenc attaches their `{...}` groups as real
|
|
55
|
+
arguments instead of leaving them as unrelated sibling nodes.
|
|
56
|
+
- `symbols.py` — the LaTeX-command → AsciiMath2-token tables. Ground truth
|
|
57
|
+
is `asciimath-parser`'s own `SYMBOLMAP`
|
|
58
|
+
(`packages/core/src/symbols.ts`), read directly from source, not
|
|
59
|
+
paraphrased from docs.
|
|
60
|
+
- `render.py` — flattens a pylatexenc node tree into a linear token stream,
|
|
61
|
+
then does a single left-to-right pass building AsciiMath2 text. `^`/`_`
|
|
62
|
+
bind to exactly the token before/after them; any compound expression used
|
|
63
|
+
as a required single argument gets wrapped in `(...)`, which AsciiMath2's
|
|
64
|
+
own grammar strips invisibly in exactly those positions — so this
|
|
65
|
+
converter never has to reimplement LaTeX operator precedence itself.
|
|
66
|
+
- `cli.py` — `asciimath2 '<latex>'` / reads stdin.
|
|
67
|
+
|
|
68
|
+
## Scope, honestly
|
|
69
|
+
|
|
70
|
+
This is a broad, tested common subset of LaTeX math — not a general LaTeX
|
|
71
|
+
interpreter (that's not a well-defined target; LaTeX can define arbitrary
|
|
72
|
+
macros). What's covered: arithmetic, fractions/roots, sub/superscripts,
|
|
73
|
+
`\left`/`\right` delimiters (all standard pairs), big operators with limits,
|
|
74
|
+
trig/log/etc. functions, Greek letters, relations/logic/arrows, fonts and
|
|
75
|
+
accents, `\text`, `\binom`/`\operatorname`/`\pmod`/`\color`, and matrix-like
|
|
76
|
+
and aligned-like environments (`pmatrix`/`bmatrix`/`vmatrix`/`Vmatrix`/
|
|
77
|
+
`matrix`/`array`/`cases`, `aligned`/`align`/`gather`/`split`/...).
|
|
78
|
+
|
|
79
|
+
Two deliberate choices about what happens outside that:
|
|
80
|
+
|
|
81
|
+
- **`tex(...)` escape hatch** — an unrecognized *bare* macro with no
|
|
82
|
+
arguments (e.g. `\digamma`) degrades to `tex(\digamma)`, AsciiMath2's own
|
|
83
|
+
raw-LaTeX passthrough (SPEC.md §2.4), rather than failing the whole
|
|
84
|
+
expression.
|
|
85
|
+
- **On failure, raise, don't guess** — anything else unsupported (an unknown
|
|
86
|
+
macro that's ambiguous about whether it takes arguments, an unrecognized
|
|
87
|
+
environment, a `\left` with no matching `\right`, ...) raises
|
|
88
|
+
`UnsupportedLatexError` for the *whole* expression rather than silently
|
|
89
|
+
emitting something plausible-looking but wrong. This is the same contract
|
|
90
|
+
mark-vector's current `backend/app/latex_to_asciimath.py` already expects
|
|
91
|
+
from `py-asciimath` (`None`/exception on failure → keep the original LaTeX
|
|
92
|
+
visible via `\(...\)`/`\[...\]`), so this package is a drop-in candidate
|
|
93
|
+
for that integration point if/when it's wired in — see "Not yet done"
|
|
94
|
+
below.
|
|
95
|
+
|
|
96
|
+
## Integration status
|
|
97
|
+
|
|
98
|
+
Both follow-ups from the initial version of this package are now done:
|
|
99
|
+
|
|
100
|
+
- **Wired into mark-vector's backend.** `backend/app/latex_to_asciimath.py`
|
|
101
|
+
uses this package (added as a local editable dependency in
|
|
102
|
+
`backend/pyproject.toml`) instead of `py-asciimath` — which is the whole
|
|
103
|
+
reason this package exists (`py-asciimath` couldn't do matrices, `\\`,
|
|
104
|
+
and other constructs this package handles). Multi-row output (aligned
|
|
105
|
+
systems, etc.) is preserved as blank-line-separated rows inside the
|
|
106
|
+
`$$...$$` block it writes back to the document, rather than being
|
|
107
|
+
collapsed to one line, so the row structure survives to the frontend.
|
|
108
|
+
- **Frontend renders AsciiMath2's extensions.** `frontend/static/js/markdown.js`
|
|
109
|
+
imports `asciimath-parser` directly as an ES module from jsDelivr
|
|
110
|
+
(`https://cdn.jsdelivr.net/npm/asciimath-parser@<version>/dist/index.js` —
|
|
111
|
+
it ships no browser-global/UMD build, unlike the `asciimath2tex` package it
|
|
112
|
+
replaced) instead of `asciimath2tex`, which only implemented the base
|
|
113
|
+
asciimath.org grammar. For the AsciiMath dialect, a full `$$...$$` block is
|
|
114
|
+
now handed to the parser in one call rather than being split line-by-line,
|
|
115
|
+
so `asciimath-parser`'s own row/`&`-alignment handling (SPEC.md §2.2) does
|
|
116
|
+
the work instead of the frontend approximating it with a `gathered`
|
|
117
|
+
environment. The LaTeX dialect's per-line `gathered` stacking is
|
|
118
|
+
unchanged, since LaTeX has no equivalent convention.
|
|
119
|
+
|
|
120
|
+
Verified end to end (`asciimath2.convert()` output fed through the actual
|
|
121
|
+
published `asciimath-parser@0.6.11` and, in a real browser via Playwright,
|
|
122
|
+
through KaTeX) for derivatives/limits, matrix multiplication, aligned
|
|
123
|
+
systems of equations, piecewise `cases`, and norms — no parse errors,
|
|
124
|
+
correct LaTeX, correct rendering.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "asciimath2"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "LaTeX -> AsciiMath2 conversion (AsciiMath2 = asciimath.org + the asciimath-parser extensions, see SPEC.md)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"latex",
|
|
11
|
+
"asciimath",
|
|
12
|
+
"asciimath2",
|
|
13
|
+
"math",
|
|
14
|
+
"converter",
|
|
15
|
+
"markup",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
26
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["pylatexenc>=2.11"]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "Alex Lee"
|
|
33
|
+
email = "iasandcb@gmail.com"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/iasandcb/asciimath2"
|
|
37
|
+
Repository = "https://github.com/iasandcb/asciimath2"
|
|
38
|
+
Issues = "https://github.com/iasandcb/asciimath2/issues"
|
|
39
|
+
Specification = "https://github.com/iasandcb/asciimath2/blob/main/SPEC.md"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
asciimath2 = "asciimath2.cli:main"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.12.4,<0.13.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = ["pytest>=9.1.1"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "asciimath2"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "LaTeX -> AsciiMath2 conversion (AsciiMath2 = asciimath.org + the asciimath-parser extensions, see SPEC.md)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Alex Lee", email = "iasandcb@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["latex", "asciimath", "asciimath2", "math", "converter", "markup"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
22
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pylatexenc>=2.11",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/iasandcb/asciimath2"
|
|
31
|
+
Repository = "https://github.com/iasandcb/asciimath2"
|
|
32
|
+
Issues = "https://github.com/iasandcb/asciimath2/issues"
|
|
33
|
+
Specification = "https://github.com/iasandcb/asciimath2/blob/main/SPEC.md"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
asciimath2 = "asciimath2.cli:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.12.4,<0.13.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=9.1.1",
|
|
45
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""AsciiMath2: LaTeX -> AsciiMath2 conversion.
|
|
2
|
+
|
|
3
|
+
See SPEC.md (repo root) for the AsciiMath2 standard this implements.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
from .errors import UnsupportedLatexError
|
|
8
|
+
from .render import convert
|
|
9
|
+
|
|
10
|
+
__all__ = ["convert", "Config", "UnsupportedLatexError"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .errors import UnsupportedLatexError
|
|
5
|
+
from .render import convert
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="asciimath2",
|
|
11
|
+
description="Convert a LaTeX math expression to AsciiMath2 (see SPEC.md).",
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"latex", nargs="?", help="LaTeX expression. Reads stdin if omitted."
|
|
15
|
+
)
|
|
16
|
+
args = parser.parse_args(argv)
|
|
17
|
+
|
|
18
|
+
latex = args.latex if args.latex is not None else sys.stdin.read()
|
|
19
|
+
try:
|
|
20
|
+
print(convert(latex))
|
|
21
|
+
except UnsupportedLatexError as e:
|
|
22
|
+
print(f"error: {e}", file=sys.stderr)
|
|
23
|
+
return 1
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class Config:
|
|
6
|
+
"""Mirrors the three render-time knobs from
|
|
7
|
+
widcardw/asciimath-parser#22 (see SPEC.md section 2.2), plus two
|
|
8
|
+
extension points for macros this package doesn't know about out of the
|
|
9
|
+
box.
|
|
10
|
+
|
|
11
|
+
Only `single_newline_break` changes what this library *emits* (it
|
|
12
|
+
controls how many newlines separate rows of a multi-line expression).
|
|
13
|
+
`multiline_env` and `bar_as_mid` are downstream-rendering choices made
|
|
14
|
+
when the AsciiMath2 text is turned back into LaTeX; they're recorded
|
|
15
|
+
here purely so a caller can pass them through to whatever renders the
|
|
16
|
+
AsciiMath2 output, without needing a second place to look them up.
|
|
17
|
+
|
|
18
|
+
`custom_macros` and `custom_text` let a caller extend the fixed
|
|
19
|
+
`symbols.py` tables per-conversion without forking this package.
|
|
20
|
+
Neither ever *overrides* a macro this package already gives structural
|
|
21
|
+
meaning to (an ignored sizing command, an accent, a font, ...) - both
|
|
22
|
+
are consulted only where the built-in tables have nothing to say, so an
|
|
23
|
+
empty (default) value is always a strict no-op:
|
|
24
|
+
|
|
25
|
+
- `custom_macros`: bare, no-argument LaTeX macro name (no backslash) ->
|
|
26
|
+
AsciiMath2 token, e.g. `{"sqcup": "suu"}` for `\\sqcup`. Consulted only
|
|
27
|
+
for a macro name none of the built-in tables recognize.
|
|
28
|
+
- `custom_text`: exact `\\text{...}`/`\\operatorname{...}`/
|
|
29
|
+
`\\operatorname*{...}` argument content -> AsciiMath2 token, e.g.
|
|
30
|
+
`{"pmf": "pmf"}` to make `\\text{pmf}` emit the bare word `pmf` instead
|
|
31
|
+
of the default quoted `"pmf"` (still correct AsciiMath2, just not
|
|
32
|
+
round-trippable through a renderer that doesn't know `pmf` is a
|
|
33
|
+
symbol of its own). Checked before the default text/operatorname
|
|
34
|
+
handling, since that default has no structural meaning of its own to
|
|
35
|
+
protect.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
multiline_env: str = "aligned"
|
|
39
|
+
single_newline_break: bool = False
|
|
40
|
+
bar_as_mid: bool = True
|
|
41
|
+
custom_macros: dict[str, str] = field(default_factory=dict)
|
|
42
|
+
custom_text: dict[str, str] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def row_break(self) -> str:
|
|
46
|
+
return "\n" if self.single_newline_break else "\n\n"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class UnsupportedLatexError(Exception):
|
|
2
|
+
"""Raised when a LaTeX construct has no known AsciiMath2 rendering.
|
|
3
|
+
|
|
4
|
+
Carries the offending macro/environment name so callers can decide
|
|
5
|
+
whether to degrade the whole expression to raw LaTeX (the safe default)
|
|
6
|
+
or report it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, what: str, detail: str = ""):
|
|
10
|
+
self.what = what
|
|
11
|
+
self.detail = detail
|
|
12
|
+
message = f"unsupported LaTeX construct: {what}"
|
|
13
|
+
if detail:
|
|
14
|
+
message += f" ({detail})"
|
|
15
|
+
super().__init__(message)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Extends pylatexenc's default macro database with argument specs for
|
|
2
|
+
macros it doesn't already know the arity of, so their `{...}` groups are
|
|
3
|
+
attached to the macro node (`nodeargd.argnlist`) instead of appearing as
|
|
4
|
+
unrelated sibling groups in the node stream.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pylatexenc import macrospec
|
|
8
|
+
from pylatexenc.latexwalker import LatexWalker, get_default_latex_context_db
|
|
9
|
+
|
|
10
|
+
_EXTRA_MACROS = [
|
|
11
|
+
macrospec.MacroSpec("binom", "{{"),
|
|
12
|
+
macrospec.MacroSpec("dbinom", "{{"),
|
|
13
|
+
macrospec.MacroSpec("tbinom", "{{"),
|
|
14
|
+
macrospec.MacroSpec("overset", "{{"),
|
|
15
|
+
macrospec.MacroSpec("underset", "{{"),
|
|
16
|
+
macrospec.MacroSpec("stackrel", "{{"),
|
|
17
|
+
macrospec.MacroSpec("boldsymbol", "{"),
|
|
18
|
+
# `\operatorname*` is not a distinct macro name to pylatexenc's lexer -
|
|
19
|
+
# macro names never include punctuation, so the lexer always reads just
|
|
20
|
+
# "operatorname" and leaves any "*" as a separate following token. A
|
|
21
|
+
# macro that takes an optional star flag must say so in its own argspec
|
|
22
|
+
# (`*` here) instead of trying to register a second "operatorname*"
|
|
23
|
+
# macro name, which pylatexenc would never actually match (verified: it
|
|
24
|
+
# silently split `\operatorname*{X}` into a zero-arg `\operatorname`
|
|
25
|
+
# plus a stray literal "*" plus an unrelated bare `{X}` group before this
|
|
26
|
+
# fix). The star itself carries no AsciiMath2-visible distinction (same
|
|
27
|
+
# "display-style detail this package doesn't model" trade-off as
|
|
28
|
+
# \dfrac/\tfrac/\cfrac all collapsing to plain `/`), so render.py's
|
|
29
|
+
# `_render_operatorname` just ignores argnlist[0] and reads the mandatory
|
|
30
|
+
# group from argnlist[1].
|
|
31
|
+
macrospec.MacroSpec("operatorname", "*{"),
|
|
32
|
+
macrospec.MacroSpec("pmod", "{"),
|
|
33
|
+
macrospec.MacroSpec("bmod", ""),
|
|
34
|
+
macrospec.MacroSpec("cfrac", "{{"),
|
|
35
|
+
macrospec.MacroSpec("dfrac", "{{"),
|
|
36
|
+
macrospec.MacroSpec("tfrac", "{{"),
|
|
37
|
+
macrospec.MacroSpec("color", "{{"),
|
|
38
|
+
macrospec.MacroSpec("textcolor", "{{"),
|
|
39
|
+
macrospec.MacroSpec("boxed", "{"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_context_db() -> macrospec.LatexContextDb:
|
|
44
|
+
db = get_default_latex_context_db()
|
|
45
|
+
db.add_context_category("asciimath2-extra", prepend=True, macros=_EXTRA_MACROS)
|
|
46
|
+
return db
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def make_walker(latex: str) -> LatexWalker:
|
|
50
|
+
return LatexWalker(latex, latex_context=build_context_db())
|
|
File without changes
|