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
21 changes: 13 additions & 8 deletions rich/_emoji_replace.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

from typing import Any, Callable, Dict, Match, Optional
import re
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional

if TYPE_CHECKING:
from typing import Match

_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
_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

_EMOJI = None
_EMOJI_SUB = None
_VARIANTS = {"text": "\uFE0E", "emoji": "\uFE0F"}


Expand All @@ -31,14 +33,17 @@ def __getattr__(name: str) -> Any:
def _emoji_replace(
text: str,
default_variant: Optional[str] = None,
_emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub,
) -> str:
"""Replace emoji code in text."""
global _EMOJI
global _EMOJI, _EMOJI_SUB
if _EMOJI is None:
from ._emoji_codes import EMOJI

_EMOJI = EMOJI
if _EMOJI_SUB is None:
import re

_EMOJI_SUB = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub
get_emoji = _EMOJI.__getitem__
get_variant = _VARIANTS.get
default_variant_code = _VARIANTS.get(default_variant, "") if default_variant else ""
Expand All @@ -52,4 +57,4 @@ def do_replace(match: Match[str]) -> str:
except KeyError:
return emoji_code

return _emoji_sub(do_replace, text)
return _EMOJI_SUB(do_replace, text)
12 changes: 8 additions & 4 deletions rich/_wrap.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from __future__ import annotations

import re
from typing import Iterable

from ._loop import loop_last
from .cells import cell_len, chop_cells

re_word = re.compile(r"\s*\S+\s*")
_re_word = None


def words(text: str) -> Iterable[tuple[int, int, str]]:
"""Yields each word from the text as a tuple
containing (start_index, end_index, word). A "word" in this context may
include the actual word and any whitespace to the right.
"""
global _re_word
if _re_word is None:
import re

_re_word = re.compile(r"\s*\S+\s*")
position = 0
word_match = re_word.match(text, position)
word_match = _re_word.match(text, position)
while word_match is not None:
start, end = word_match.span()
word = word_match.group(0)
yield start, end, word
word_match = re_word.match(text, end)
word_match = _re_word.match(text, end)


def divide_line(text: str, width: int, fold: bool = True) -> list[int]:
Expand Down
46 changes: 32 additions & 14 deletions rich/color.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import re
from __future__ import annotations

import sys
from colorsys import rgb_to_hls
from enum import IntEnum
from functools import lru_cache
from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple

from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
from .color_triplet import ColorTriplet
from .repr import Result, rich_repr
from .terminal_theme import DEFAULT_TERMINAL_THEME
from .repr import rich_repr

if TYPE_CHECKING:
from .repr import Result
from .terminal_theme import TerminalTheme
from .text import Text

if TYPE_CHECKING: # pragma: no cover
from .terminal_theme import TerminalTheme
Expand Down Expand Up @@ -289,14 +292,7 @@ class ColorParseError(Exception):
"""The color could not be parsed."""


RE_COLOR = re.compile(
r"""^
\#([0-9a-f]{6})$|
color\(([0-9]{1,3})\)$|
rgb\(([\d\s,]+)\)$
""",
re.VERBOSE,
)
_RE_COLOR = None


@rich_repr
Expand Down Expand Up @@ -360,7 +356,11 @@ def get_truecolor(
"""

if theme is None:
from .terminal_theme import DEFAULT_TERMINAL_THEME

theme = DEFAULT_TERMINAL_THEME
from ._palettes import EIGHT_BIT_PALETTE, WINDOWS_PALETTE

if self.type == ColorType.TRUECOLOR:
assert self.triplet is not None
return self.triplet
Expand Down Expand Up @@ -446,7 +446,19 @@ def parse(cls, color: str) -> "Color":
number=color_number,
)

color_match = RE_COLOR.match(color)
global _RE_COLOR
if _RE_COLOR is None:
import re

_RE_COLOR = re.compile(
r"""^
\#([0-9a-f]{6})$|
color\(([0-9]{1,3})\)$|
rgb\(([\d\s,]+)\)$
""",
re.VERBOSE,
)
color_match = _RE_COLOR.match(color)
if color_match is None:
raise ColorParseError(f"{original_color!r} is not a valid color")

Expand Down Expand Up @@ -515,6 +527,10 @@ def downgrade(self, system: ColorSystem) -> "Color":

if self.type in (ColorType.DEFAULT, system):
return self
from colorsys import rgb_to_hls

from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE

# Convert to 8-bit color from truecolor color
if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR:
assert self.triplet is not None
Expand Down Expand Up @@ -613,6 +629,8 @@ def blend_rgb(
if color_number < 16:
table.add_row(color_cell, f"{color_number}", Text(f'"{name}"'))
else:
from ._palettes import EIGHT_BIT_PALETTE

color = EIGHT_BIT_PALETTE[color_number] # type: ignore[has-type]
table.add_row(
color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb
Expand Down
7 changes: 4 additions & 3 deletions rich/default_styles.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict

from .color import Color
from .style import Style

DEFAULT_STYLES: Dict[str, Style] = {
Expand Down Expand Up @@ -124,9 +125,9 @@
"traceback.note": Style(color="green", bold=True),
"traceback.group.border": Style(color="magenta"),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
"bar.pulse": Style(color="rgb(249,38,114)"),
"bar.complete": Style(color=Color.from_rgb(249, 38, 114)),
"bar.finished": Style(color=Color.from_rgb(114, 156, 31)),
"bar.pulse": Style(color=Color.from_rgb(249, 38, 114)),
"progress.description": Style.null(),
"progress.filesize": Style(color="green"),
"progress.filesize.total": Style(color="green"),
Expand Down
3 changes: 2 additions & 1 deletion rich/highlighter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
from abc import ABC, abstractmethod
from typing import ClassVar, Sequence, Union

Expand Down Expand Up @@ -123,6 +122,8 @@ class JSONHighlighter(RegexHighlighter):
def highlight(self, text: Text) -> None:
super().highlight(text)

import re

# Additional work to handle highlighting JSON keys
plain = text.plain
append = text.spans.append
Expand Down
51 changes: 37 additions & 14 deletions rich/markup.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import re
from __future__ import annotations

from operator import attrgetter
from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union
from typing import TYPE_CHECKING, Callable, Iterable, List, NamedTuple, Optional, Tuple, Union

if TYPE_CHECKING:
from typing import Match

from ._emoji_replace import _emoji_replace
from .emoji import EmojiVariant
from .errors import MarkupError
from .style import Style
from .text import Span, Text

RE_TAGS = re.compile(
r"""((\\*)\[([a-z#/@][^[]*?)])""",
re.VERBOSE,
)
_RE_TAGS = None
_RE_HANDLER = None


def _compile_tags():
global _RE_TAGS
if _RE_TAGS is None:
import re

RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")
_RE_TAGS = re.compile(
r"""((\\*)\[([a-z#/@][^[]*?)])""",
re.VERBOSE,
)
return _RE_TAGS


class Tag(NamedTuple):
Expand All @@ -39,14 +51,15 @@ 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
_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

_RE_ESCAPE = None


def escape(
markup: str,
_escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub,
) -> str:
"""Escapes text so that it won't be interpreted as markup.

Expand All @@ -56,13 +69,18 @@ def escape(
Returns:
str: Markup with square brackets escaped.
"""
global _RE_ESCAPE
if _RE_ESCAPE is None:
import re

_RE_ESCAPE = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub

def escape_backslashes(match: Match[str]) -> str:
"""Called by re.sub replace matches."""
backslashes, text = match.groups()
return f"{backslashes}{backslashes}\\{text}"

markup = _escape(escape_backslashes, markup)
markup = _RE_ESCAPE(escape_backslashes, markup)
if markup.endswith("\\") and not markup.endswith("\\\\"):
return markup + "\\"

Expand All @@ -79,7 +97,7 @@ def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:
position = 0
_divmod = divmod
_Tag = Tag
for match in RE_TAGS.finditer(markup):
for match in _compile_tags().finditer(markup):
full_text, escapes, tag_text = match.groups()
start, end = match.span()
if start > position:
Expand Down Expand Up @@ -178,7 +196,12 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
if open_tag.parameters:
handler_name = ""
parameters = open_tag.parameters.strip()
handler_match = RE_HANDLER.match(parameters)
global _RE_HANDLER
if _RE_HANDLER is None:
import re

_RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")
handler_match = _RE_HANDLER.match(parameters)
if handler_match is not None:
handler_name, match_parameters = handler_match.groups()
parameters = (
Expand Down
2 changes: 1 addition & 1 deletion rich/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:

_Segment = Segment
for segment in iter_segments:
if last_segment.style == segment.style and not segment.control:
if (last_segment.style is segment.style or last_segment.style == segment.style) and not segment.control:
last_segment = _Segment(
last_segment.text + segment.text, last_segment.style
)
Expand Down
14 changes: 10 additions & 4 deletions rich/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,12 @@ def __rich_repr__(self) -> Result:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return self.__hash__() == other.__hash__()
return self is other or self.__hash__() == other.__hash__()

def __ne__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return self.__hash__() != other.__hash__()
return self is not other and self.__hash__() != other.__hash__()

def __hash__(self) -> int:
if self._hash is not None:
Expand Down Expand Up @@ -624,7 +624,10 @@ def combine(cls, styles: Iterable["Style"]) -> "Style":
Style: A new style instance.
"""
iter_styles = iter(styles)
return sum(iter_styles, next(iter_styles))
combined = next(iter_styles)
for style in iter_styles:
combined = combined._add(style)
return combined

@classmethod
def chain(cls, *styles: "Style") -> "Style":
Expand All @@ -637,7 +640,10 @@ def chain(cls, *styles: "Style") -> "Style":
Style: A new style instance.
"""
iter_styles = iter(styles)
return sum(iter_styles, next(iter_styles))
combined = next(iter_styles)
for style in iter_styles:
combined = combined._add(style)
return combined

def copy(self) -> "Style":
"""Get a copy of this style.
Expand Down
Loading