Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions lib/matplotlib/_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
from numpy.typing import NDArray
from pyparsing import (
Empty, Forward, Literal, Group, NotAny, OneOrMore, Optional,
ParseBaseException, ParseExpression, ParseFatalException,
ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore,
pyparsing_common, nested_expr, one_of)
ParseBaseException, ParseException, ParseExpression, ParseFatalException,
ParserElement, ParseResults, QuotedString, Regex, StringEnd, Token,
ZeroOrMore, pyparsing_common, nested_expr, one_of)

import matplotlib as mpl
from . import cbook
Expand Down Expand Up @@ -1873,6 +1873,51 @@ def raise_error(s: str, loc: int, toks: ParseResults) -> T.Any:
return Empty().set_parse_action(raise_error)


class _BracedText(Token):
r"""
Match a brace-delimited literal string, allowing nested braces.

This is similar to ``QuotedString("{", "\\", end_quote_char="}")``, except
that brace depth is tracked, so that the string does not end at the first
``}``. As in TeX, nested unescaped braces only group, and are not
rendered; a literal brace is written as ``\{`` or ``\}``. A backslash
escapes the following character, which therefore does not affect depth.
"""

_escapes = {"t": "\t", "n": "\n", "f": "\f", "r": "\r"}

def __init__(self) -> None:
super().__init__()
self.mayReturnEmpty = True
self.mayIndexError = False
self.errmsg = "Expected '{'"

def parseImpl(self, instring: str, loc: int,
do_actions: bool = True) -> tuple[int, str]:
if loc >= len(instring) or instring[loc] != "{":
raise ParseException(instring, loc, self.errmsg, self)
chars = []
depth = 0
while loc < len(instring):
char = instring[loc]
if char == "\\" and loc + 1 < len(instring):
escaped = instring[loc + 1]
chars.append(self._escapes.get(escaped, escaped))
loc += 2
continue
loc += 1
if char == "{":
depth += 1
continue
elif char == "}":
depth -= 1
if depth == 0:
return loc, "".join(chars)
continue
chars.append(char)
raise ParseException(instring, loc, "Expected '}'", self)


class ParserState:
"""
Parser state.
Expand Down Expand Up @@ -2216,7 +2261,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex:
r"\underset",
p.optional_group("annotation") + p.optional_group("body"))

p.text = cmd(r"\text", QuotedString('{', '\\', end_quote_char="}"))
p.text = cmd(r"\text", _BracedText())

p.substack = cmd(r"\substack",
nested_expr(opener="{", closer="}",
Expand Down
16 changes: 16 additions & 0 deletions lib/matplotlib/tests/test_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,22 @@ def test_mathtext_single_char_super_with_prime(expr):
parser.parse(expr)


@check_figures_equal()
def test_text_nested_braces(fig_test, fig_ref):
# Nested braces group as in TeX, and are not rendered (gh-32105).
fig_test.text(0.1, 0.2, r"$\text{{example}}$")
fig_test.text(0.1, 0.5, r"$\text{a{b}{{c}}d}$")
fig_ref.text(0.1, 0.2, r"$\text{example}$")
fig_ref.text(0.1, 0.5, r"$\text{abcd}$")


@check_figures_equal()
def test_text_escaped_braces(fig_test, fig_ref):
# Escaped braces are still rendered as literal braces (gh-32105).
fig_test.text(0.1, 0.2, r"$\text{{\{example\}}}$")
fig_ref.text(0.1, 0.2, r"$\text{\{example\}}$")


@check_figures_equal()
def test_boldsymbol(fig_test, fig_ref):
fig_test.text(0.1, 0.2, r"$\boldsymbol{\mathrm{abc0123\alpha}}$")
Expand Down
Loading