Skip to content

Commit

Permalink
rich: svg: workaround truncated last captured output line
Browse files Browse the repository at this point in the history
Workaround upstream rich library issue 3576:
Underscores on the last captured line are clipped
when exporting to SVG.

The main <clipPath> height computed
by the rich library in Console.export_svg() is a bit too small,
and the bottom of the last captured output line may be truncated,
depending on the font and the printed characters (e.g. underscores,
but also the bottom of the letter 'y').

This workaround adds the bottom padding to the height computed
by the rich library.

Ref:
- #8
- Textualize/rich#3576
  • Loading branch information
dottspina committed Dec 9, 2024
1 parent a94ebdf commit e3f9828
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 2 deletions.
49 changes: 49 additions & 0 deletions src/dtsh/rich/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ def __init__(self, cause: str) -> None:
# down to the captured output (aka the "Terminal group").
PADDING_TOP: int = GTITLE_Y - 1

# Bottom pading.
PADDING_BOTTOM: int = 8

# Y coordinate of the SVG group representing the actual capture.
# as generated by the rich library (aka the "Terminal group").
GTERM_Y = 41
Expand Down Expand Up @@ -415,6 +418,52 @@ class SVGFragmentDefs(SVGFragment):
RE_BEGIN = SVGFormat.RE_DEFS
RE_END = SVGFormat.RE_DEFS_CLOSE

# RE matching the first <rect> that will appear within
# the SVG <defs> element.
# See _workaround_rich_issue_3576().
RE_RECT_3576 = re.compile(
r'\s*<rect\s+.*x="0" y="0" width="[\d.]+" height="(?P<h>[\d.]+)"'
)

def __init__(
self, endl: int, content: SVGText, matched: re.Match[str]
) -> None:
"""Initialize rectangle geometry."""
super().__init__(endl, content, matched)
self._workaround_rich_issue_3576()

def _workaround_rich_issue_3576(self) -> None:
# Workaround upstream rich library issue 3576:
# Underscores on the last captured line are clipped
# when exporting to SVG.
#
# The main <clipPath> height computed
# by the rich library in Console.export_svg() is a bit too small,
# and the bottom of the last captured output line may be truncated,
# depending on the font and the printed characters (e.g. underscores,
# but also the bottom of the letter 'y').
#
# This workaround adds the bottom padding to the height computed
# by the rich library.
#
# Ref:
# - https://github.com/Textualize/rich/issues/3576
matched: Optional[re.Match[str]] = None
i_rect: int = 0
for i_rect, txt in enumerate(self._content):
matched = SVGFragmentDefs.RE_RECT_3576.match(txt)
if matched:
break
if not matched:
raise SVGFormat.Error(SVGFragmentDefs.RE_RECT_3576.pattern)

height = float(matched.group("h"))
# Taking account for the missing bottom padding.
height += SVGFormat.PADDING_BOTTOM
self._content[i_rect] = re.sub(
r'height="[\d.]+"', f'height="{height}"', self._content[i_rect]
)

def append(self, fragment: "SVGFragmentDefs") -> None:
"""Append SVG clipping paths from another fragment (append nmode).
Expand Down
11 changes: 9 additions & 2 deletions tests/test_dtsh_rich_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,22 @@ def test_svg_fragment_defs() -> None:
[
"<!-- before -->",
"<defs>",
'<clipPath id="terminal-257872858-clip-terminal">',
'<rect x="0" y="0" width="755.4" height="299.4" />',
"</clipPath>",
"...",
"</defs>",
"<!-- after -->",
]
)
assert 3 == fragment.i_end
assert 3 == len(fragment.content)
assert 6 == fragment.i_end
assert 6 == len(fragment.content)
assert [
"<defs>",
'<clipPath id="terminal-257872858-clip-terminal">',
# 307.4 = 299.4 + 8 (rich library issue 3576)
'<rect x="0" y="0" width="755.4" height="307.4" />',
"</clipPath>",
"...",
"</defs>",
] == fragment.content
Expand Down

0 comments on commit e3f9828

Please sign in to comment.