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.
@@ -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