diff options
Diffstat (limited to 'libs/rich/markup.py')
-rw-r--r-- | libs/rich/markup.py | 99 |
1 files changed, 81 insertions, 18 deletions
diff --git a/libs/rich/markup.py b/libs/rich/markup.py index 57ed0eaf7..58903f6bb 100644 --- a/libs/rich/markup.py +++ b/libs/rich/markup.py @@ -1,17 +1,22 @@ +from ast import literal_eval +from operator import attrgetter import re -from typing import Iterable, List, Match, NamedTuple, Optional, Tuple, Union +from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union from .errors import MarkupError from .style import Style from .text import Span, Text +from .emoji import EmojiVariant from ._emoji_replace import _emoji_replace RE_TAGS = re.compile( - r"""((\\*)\[([a-z#\/].*?)\])""", + r"""((\\*)\[([a-z#\/@].*?)\])""", re.VERBOSE, ) +RE_HANDLER = re.compile(r"^([\w\.]*?)(\(.*?\))?$") + class Tag(NamedTuple): """A tag in console markup.""" @@ -36,7 +41,14 @@ class Tag(NamedTuple): ) -def escape(markup: str, _escape=re.compile(r"(\\*)(\[[a-z#\/].*?\])").sub) -> 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 + + +def escape( + markup: str, _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#\/@].*?\])").sub +) -> str: """Escapes text so that it won't be interpreted as markup. Args: @@ -88,7 +100,12 @@ def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]: yield position, markup[position:], None -def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Text: +def render( + markup: str, + style: Union[str, Style] = "", + emoji: bool = True, + emoji_variant: Optional[EmojiVariant] = None, +) -> Text: """Render console markup in to a Text instance. Args: @@ -103,7 +120,10 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te """ emoji_replace = _emoji_replace if "[" not in markup: - return Text(emoji_replace(markup) if emoji else markup, style=style) + return Text( + emoji_replace(markup, default_variant=emoji_variant) if emoji else markup, + style=style, + ) text = Text(style=style) append = text.append normalize = Style.normalize @@ -130,6 +150,7 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te elif tag is not None: if tag.name.startswith("/"): # Closing tag style_name = tag.name[1:].strip() + if style_name: # explicit close style_name = normalize(style_name) try: @@ -146,7 +167,47 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te f"closing tag '[/]' at position {position} has nothing to close" ) from None - append_span(_Span(start, len(text), str(open_tag))) + if open_tag.name.startswith("@"): + if open_tag.parameters: + handler_name = "" + parameters = open_tag.parameters.strip() + handler_match = RE_HANDLER.match(parameters) + if handler_match is not None: + handler_name, match_parameters = handler_match.groups() + parameters = ( + "()" if match_parameters is None else match_parameters + ) + + try: + meta_params = literal_eval(parameters) + except SyntaxError as error: + raise MarkupError( + f"error parsing {parameters!r} in {open_tag.parameters!r}; {error.msg}" + ) + except Exception as error: + raise MarkupError( + f"error parsing {open_tag.parameters!r}; {error}" + ) from None + + if handler_name: + meta_params = ( + handler_name, + meta_params + if isinstance(meta_params, tuple) + else (meta_params,), + ) + + else: + meta_params = () + + append_span( + _Span( + start, len(text), Style(meta={open_tag.name: meta_params}) + ) + ) + else: + append_span(_Span(start, len(text), str(open_tag))) + else: # Opening tag normalized_tag = _Tag(normalize(tag.name), tag.parameters) style_stack.append((len(text), normalized_tag)) @@ -158,24 +219,26 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te if style: append_span(_Span(start, text_length, style)) - text.spans = sorted(spans) + text.spans = sorted(spans[::-1], key=attrgetter("start")) return text if __name__ == "__main__": # pragma: no cover - from rich.console import Console - from rich.text import Text + MARKUP = [ + "[red]Hello World[/red]", + "[magenta]Hello [b]World[/b]", + "[bold]Bold[italic] bold and italic [/bold]italic[/italic]", + "Click [link=https://www.willmcgugan.com]here[/link] to visit my Blog", + ":warning-emoji: [bold red blink] DANGER![/]", + ] - console = Console(highlight=False) + from rich.table import Table + from rich import print - console.print("Hello [1], [1,2,3] ['hello']") - console.print("foo") - console.print("Hello [link=https://www.willmcgugan.com]W[b red]o[/]rld[/]!") + grid = Table("Markup", "Result", padding=(0, 1)) - from rich import print + for markup in MARKUP: + grid.add_row(Text(markup), markup) - print(escape("[red]")) - print(escape(r"\[red]")) - print(escape(r"\\[red]")) - print(escape(r"\\\[red]")) + print(grid) |