Skip to content
Merged
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
11 changes: 11 additions & 0 deletions doc/release/next_whats_new/tex_phantoms.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mathtext support for ``\phantom``, ``\llap``, ``\rlap``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mathtext gained support for the TeX macros ``\phantom``, ``\llap``, and
``\rlap``. ``\phantom`` allows to occupy some space on the canvas as if
some text was being rendered, without actually rendering that text, whereas
``\llap`` and ``\rlap`` allows to render some text on the canvas while
pretending that it occupies no space. Altogether these macros allow some finer
control of text alignments.

See https://www.tug.org/TUGboat/tb22-4/tb72perlS.pdf for a detailed description
of these macros.
41 changes: 33 additions & 8 deletions lib/matplotlib/_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,7 @@ def __init__(self, elements: T.Sequence[Node], w: float = 0.0,
if do_kern:
self.kern()
self.hpack(w=w, m=m)
self.is_phantom = False

def is_char_node(self) -> bool:
# See description in Node.is_char_node.
Expand Down Expand Up @@ -1716,6 +1717,11 @@ def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output:
off_v = oy + box.height
output = Output(box)

phantom: list[bool] = []
def render(node, *args):
if not any(phantom):
node.render(*args)

def clamp(value: float) -> float:
return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value

Expand All @@ -1729,9 +1735,11 @@ def hlist_out(box: Hlist) -> None:
base_line = cur_v
left_edge = cur_h

phantom.append(box.is_phantom)

for p in box.children:
if isinstance(p, Char):
p.render(output, cur_h + off_h, cur_v + off_v)
render(p, output, cur_h + off_h, cur_v + off_v)
cur_h += p.width
elif isinstance(p, Kern):
cur_h += p.width
Expand Down Expand Up @@ -1762,9 +1770,9 @@ def hlist_out(box: Hlist) -> None:
rule_depth = box.depth
if rule_height > 0 and rule_width > 0:
cur_v = base_line + rule_depth
p.render(output,
cur_h + off_h, cur_v + off_v,
rule_width, rule_height)
render(p, output,
cur_h + off_h, cur_v + off_v,
rule_width, rule_height)
cur_v = base_line
cur_h += rule_width
elif isinstance(p, Glue):
Expand All @@ -1782,6 +1790,8 @@ def hlist_out(box: Hlist) -> None:
rule_width += cur_g
cur_h += rule_width

phantom.pop()

def vlist_out(box: Vlist) -> None:
nonlocal cur_v, cur_h

Expand Down Expand Up @@ -1821,9 +1831,9 @@ def vlist_out(box: Vlist) -> None:
rule_height += rule_depth
if rule_height > 0 and rule_depth > 0:
cur_v += rule_height
p.render(output,
cur_h + off_h, cur_v + off_v,
rule_width, rule_height)
render(p, output,
cur_h + off_h, cur_v + off_v,
rule_width, rule_height)
elif isinstance(p, Glue):
glue_spec = p.glue_spec
rule_height = glue_spec.width - cur_g
Expand Down Expand Up @@ -2159,6 +2169,10 @@ def csnames(group: str, names: Iterable[str]) -> Regex:

p.customspace = cmd(r"\hspace", "{" + p.float_literal("space") + "}")

p.phantom = cmd(r"\phantom", p.optional_group("value"))
p.llap = cmd(r"\llap", p.optional_group("value"))
p.rlap = cmd(r"\rlap", p.optional_group("value"))

p.accent = (
csnames("accent", [*self._accent_map, *self._wide_accents])
- p.named_placeable("sym"))
Expand Down Expand Up @@ -2225,7 +2239,8 @@ def csnames(group: str, names: Iterable[str]) -> Regex:
r"\boldsymbol", "{" + ZeroOrMore(p.simple)("value") + "}")

p.placeable <<= (
p.accent # Must be before symbol as all accents are symbols
p.phantom | p.llap | p.rlap
| p.accent # Must be before symbol as all accents are symbols
| p.symbol # Must be second to catch all named symbols and single
# chars not in a group
| p.function
Expand Down Expand Up @@ -2419,6 +2434,16 @@ def symbol(self, s: str, loc: int,
def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any:
raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}")

def phantom(self, toks: ParseResults) -> T.Any:
toks["value"].is_phantom = True
return toks["value"]

def llap(self, toks: ParseResults) -> T.Any:
return [Hlist([Kern(-toks["value"].width), toks["value"]])]

def rlap(self, toks: ParseResults) -> T.Any:
return [Hlist([toks["value"], Kern(-toks["value"].width)])]

_accent_map = {
r'hat': r'\circumflexaccent',
r'breve': r'\combiningbreve',
Expand Down
15 changes: 15 additions & 0 deletions lib/matplotlib/tests/test_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,21 @@ def test_mathnormal(fig_test, fig_ref):
fig_ref.text(0.1, 0.2, r"$\mathrm{0123456789}$")


# Test vector output because in raster output some minor differences remain,
# likely due to double-striking.
@check_figures_equal(extensions=["pdf"])
def test_phantoms(fig_test, fig_ref):
fig_test.text(0.5, 0.9, r"$\rlap{rlap}extra$", ha="left")
fig_ref.text(0.5, 0.9, r"$rlap$", ha="left")
fig_ref.text(0.5, 0.9, r"$extra$", ha="left")

fig_test.text(0.5, 0.8, r"$extra\llap{llap}$", ha="right")
fig_ref.text(0.5, 0.8, r"$llap$", ha="right")
fig_ref.text(0.5, 0.8, r"$extra$", ha="right")

fig_test.text(0.5, 0.7, r"$\phantom{phantom}$")


def test_box_repr():
s = repr(_mathtext.Parser().parse(
r"$\frac{1}{2}$",
Expand Down
Loading