Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emoji stripping #3524

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- emoji strip mode on emoji=False (https://github.com/Textualize/rich/issues/3520)

## [13.9.2] - 2024-10-04

### Fixed
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,4 @@ The following people have contributed to the development of Rich:
- [L. Yeung](https://github.com/lewis-yeung)
- [chthollyphile](https://github.com/chthollyphile)
- [Jonathan Helmus](https://github.com/jjhelmus)
- [DinhHuy2010](https://github.com/DinhHuy2010)
12 changes: 6 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions rich/_emoji_replace.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from typing import Callable, Match, Optional
import re
from typing import Callable, Literal, Match, Optional

from ._emoji_codes import EMOJI


_ReStringMatch = Match[str] # regex match object
_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
_EmojiSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
_StripMode = Literal["keep", "strip"]


def _emoji_replace(
text: str,
default_variant: Optional[str] = None,
_emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub,
strip: bool = False,
) -> str:
"""Replace emoji code in text."""
get_emoji = EMOJI.__getitem__
Expand All @@ -21,6 +22,8 @@ def _emoji_replace(
default_variant_code = variants.get(default_variant, "") if default_variant else ""

def do_replace(match: Match[str]) -> str:
if strip:
return ""
emoji_code, emoji_name, variant = match.groups()
try:
return get_emoji(emoji_name.lower()) + get_variant(
Expand All @@ -30,3 +33,16 @@ def do_replace(match: Match[str]) -> str:
return emoji_code

return _emoji_sub(do_replace, text)


def process_emoji_in_text(
text: str,
default_variant: Optional[str] = None,
strip_mode: Optional[_StripMode] = None,
) -> str:
"""Process text with emoji codes."""
if strip_mode is not None and strip_mode == "keep":
# fast track, keep emojicodes -> return original text
return text
should_strip = strip_mode is not None and strip_mode == "strip"
return _emoji_replace(text, default_variant, strip=should_strip)
18 changes: 12 additions & 6 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
) # pragma: no cover

from . import errors, themes
from ._emoji_replace import _emoji_replace
from ._emoji_replace import process_emoji_in_text
from ._export_format import CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT
from ._fileno import get_fileno
from ._log_render import FormatTimeCallable, LogRender
Expand Down Expand Up @@ -80,6 +80,7 @@
HighlighterType = Callable[[Union[str, "Text"]], "Text"]
JustifyMethod = Literal["default", "left", "center", "right", "full"]
OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
EmojiStripMode = Literal["keep", "strip"]


class NoChange:
Expand Down Expand Up @@ -614,6 +615,7 @@ class Console:
markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
emoji (bool, optional): Enable emoji code. Defaults to True.
emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.
strip_emoji_mode (str, optional): If emoji is False, strip emojicodes by either "keep" (keep the emojicodes), or "strip" (remove the emojicodes)
highlight (bool, optional): Enable automatic highlighting. Defaults to True.
log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
Expand Down Expand Up @@ -651,6 +653,7 @@ def __init__(
markup: bool = True,
emoji: bool = True,
emoji_variant: Optional[EmojiVariant] = None,
strip_emoji_mode: EmojiStripMode = "keep",
highlight: bool = True,
log_time: bool = True,
log_path: bool = True,
Expand Down Expand Up @@ -686,6 +689,7 @@ def __init__(
self._markup = markup
self._emoji = emoji
self._emoji_variant: Optional[EmojiVariant] = emoji_variant
self._strip_emoji_mode = strip_emoji_mode
self._highlight = highlight
self.legacy_windows: bool = (
(detect_legacy_windows() and not self.is_jupyter)
Expand Down Expand Up @@ -1424,23 +1428,25 @@ def render_str(
emoji_enabled = emoji or (emoji is None and self._emoji)
markup_enabled = markup or (markup is None and self._markup)
highlight_enabled = highlight or (highlight is None and self._highlight)
emoji_strip_mode = self._strip_emoji_mode if emoji_enabled is False else None

if markup_enabled:
rich_text = render_markup(
text,
style=style,
emoji=emoji_enabled,
emoji_variant=self._emoji_variant,
strip_emoji_mode=emoji_strip_mode,
)
rich_text.justify = justify
rich_text.overflow = overflow
else:
emojized_text = process_emoji_in_text(
text, default_variant=self._emoji_variant, strip_mode=emoji_strip_mode
)

rich_text = Text(
(
_emoji_replace(text, default_variant=self._emoji_variant)
if emoji_enabled
else text
),
emojized_text,
justify=justify,
overflow=overflow,
style=style,
Expand Down
26 changes: 21 additions & 5 deletions rich/markup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import re
from ast import literal_eval
from operator import attrgetter
from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union
from typing import (
Callable,
Iterable,
List,
Literal,
Match,
NamedTuple,
Optional,
Tuple,
Union,
)

from ._emoji_replace import _emoji_replace
from ._emoji_replace import process_emoji_in_text
from .emoji import EmojiVariant
from .errors import MarkupError
from .style import Style
Expand Down Expand Up @@ -43,6 +53,7 @@ def markup(self) -> str:
_ReStringMatch = Match[str] # regex match object
_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
_EscapeSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
_EmojiStripMode = Literal["keep", "strip"]


def escape(
Expand Down Expand Up @@ -108,6 +119,7 @@ def render(
style: Union[str, Style] = "",
emoji: bool = True,
emoji_variant: Optional[EmojiVariant] = None,
strip_emoji_mode: Optional[_EmojiStripMode] = None,
) -> Text:
"""Render console markup in to a Text instance.

Expand All @@ -116,18 +128,22 @@ def render(
style: (Union[str, Style]): The style to use.
emoji (bool, optional): Also render emoji code. Defaults to True.
emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.

strip_emoji_mode (str, optional): If emoji is False, strip emojicodes by either "keep" (keep the emojicodes), or "strip" (remove the emojicodes)

Raises:
MarkupError: If there is a syntax error in the markup.

Returns:
Text: A test instance.
"""
emoji_replace = _emoji_replace

def emoji_replace(markup: str, default_variant: Optional[str] = None) -> str:
strip_mode = None if emoji else strip_emoji_mode
return process_emoji_in_text(markup, default_variant, strip_mode=strip_mode)

if "[" not in markup:
return Text(
emoji_replace(markup, default_variant=emoji_variant) if emoji else markup,
emoji_replace(markup, default_variant=emoji_variant),
style=style,
)
text = Text(style=style)
Expand Down
22 changes: 22 additions & 0 deletions tests/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,28 @@ def test_print_text():
assert console.file.getvalue() == "\x1B[1mfoo\x1B[0m\n"


def test_print_with_keep_emojicodes():
console = Console(
file=io.StringIO(),
color_system="truecolor",
emoji=False,
strip_emoji_mode="keep",
)
console.print("Hi guys! :sparkles:", end="")
assert console.file.getvalue() == "Hi guys! :sparkles:"


def test_print_with_removing_emojicodes():
console = Console(
file=io.StringIO(),
color_system="truecolor",
emoji=False,
strip_emoji_mode="strip",
)
console.print("Hi guys! :sparkles:", end="")
assert console.file.getvalue() == "Hi guys! "


def test_print_text_multiple():
console = Console(file=io.StringIO(), color_system="truecolor")
console.print(Text("foo", style="bold"), Text("bar"), "baz")
Expand Down