Repository: matplotlib/matplotlib
Pull request: Fix mathtext mismatched braces
## PR summary
Fixes #26510

## PR checklist
<!-- Please mark any checkboxes that do not apply to this PR as [N/A].-->

- [x] "closes #0000" is in the body of the PR description to [link the related issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- [x] new and changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [ ] *Plotting related* features are demonstrated in an [example](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-examples-and-tutorials)
- [ ] *New Features* and *API Changes* are noted with a [directive and release note](https://matplotlib.org/devdocs/devel/coding_guide.html#new-features-and-api-changes)
- [ ] Documentation complies with [general](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-rest-pages) and [docstring](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-docstrings) guidelines

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at https://matplotlib.org/devdocs/devel/development_workflow.html

- Create a separate branch for your changes and open the PR from this branch. Please avoid working on `main`.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  https://matplotlib.org/stable/devel/documenting_mpl.html#formatting-conventions.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[Bug]: mathtext silently ignores content after mismatched opening brace
### Bug summary

All's in the title.

### Code for reproduction

```python
from pylab import *; figtext(.5, .5, r"$a={a$")
```


### Actual outcome

"a = " is printed.

### Expected outcome

An exception is thrown.

### Additional information

This appears at least as far back as 3.5, so is unlikely to come from the recent mathtext work.

### Operating system

macos

### Matplotlib Version

3.8.0.dev1801+g5d5700a99e

### Matplotlib Backend

any

### Python version

3.11.3

### Jupyter version

_No response_

### Installation

git checkout

Hints from issue discussion:

Commit message: Add unclosed group to grammar

Co-authored-by: Kyle Sunden <git@ksunden.space>

Patch to review:
diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py
index 25a825b7b0d9..73cb0ae1189d 100644
--- a/lib/matplotlib/_mathtext.py
+++ b/lib/matplotlib/_mathtext.py
@@ -1894,6 +1894,7 @@ def csnames(group, names):
         p.function = csnames("name", self._function_names)
 
         p.group = p.start_group + ZeroOrMore(p.token)("group") + p.end_group
+        p.unclosed_group = (p.start_group + ZeroOrMore(p.token)("group") + StringEnd())
 
         p.frac  = cmd(r"\frac", p.required_group("num") + p.required_group("den"))
         p.dfrac = cmd(r"\dfrac", p.required_group("num") + p.required_group("den"))
@@ -1942,6 +1943,7 @@ def csnames(group, names):
         p.token <<= (
             p.simple
             | p.auto_delim
+            | p.unclosed_group
             | p.unknown_symbol  # Must be last
         )
 
@@ -2030,7 +2032,8 @@ def main(self, s, loc, toks):
         return [Hlist(toks)]
 
     def math_string(self, s, loc, toks):
-        return self._math_expression.parseString(toks[0][1:-1])
+        return self._math_expression.parseString(toks[0][1:-1],
+                                                 parseAll=True)
 
     def math(self, s, loc, toks):
         hlist = Hlist(toks)
@@ -2250,6 +2253,9 @@ def end_group(self, s, loc, toks):
         self.pop_state()
         return []
 
+    def unclosed_group(self, s, loc, toks):
+        raise ParseFatalException(s, len(s), "Expected '}'")
+
     def font(self, s, loc, toks):
         self.get_state().font = toks["font"]
         return []
diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py
index d80312495d91..133469b00d8e 100644
--- a/lib/matplotlib/tests/test_mathtext.py
+++ b/lib/matplotlib/tests/test_mathtext.py
@@ -320,6 +320,7 @@ def test_fontinfo():
         (r'$a^2^2$', r'Double superscript'),
         (r'$a_2_2$', r'Double subscript'),
         (r'$a^2_a^2$', r'Double superscript'),
+        (r'$a = {b$', r"Expected '}'"),
     ],
     ids=[
         'hspace without value',
@@ -347,7 +348,8 @@ def test_fontinfo():
         'unknown symbol',
         'double superscript',
         'double subscript',
-        'super on sub without braces'
+        'super on sub without braces',
+        'unclosed group'
     ]
 )
 def test_mathtext_exceptions(math, msg):


Code review comments:
diff_hunk: @@ -2030,7 +2032,8 @@ def main(self, s, loc, toks):
         return [Hlist(toks)]
 
     def math_string(self, s, loc, toks):
-        return self._math_expression.parseString(toks[0][1:-1])
+        return self._math_expression.parseString(toks[0][1:-1],
+                                                 parseAll=True)
path: lib/matplotlib/_mathtext.py
text: @user1:
Should fit
```suggestion
        return self._math_expression.parseString(toks[0][1:-1], parseAll=True)
```

diff_hunk: @@ -347,7 +348,8 @@ def test_fontinfo():
         'unknown symbol',
         'double superscript',
         'double subscript',
-        'super on sub without braces'
+        'super on sub without braces',
+        'unclosed group'
path: lib/matplotlib/tests/test_mathtext.py
text: @user1:
So next change doesn't need to modify two lines like this time:
```suggestion
        'unclosed group',
```