aboutsummaryrefslogtreecommitdiffhomepage
path: root/libs/rich/markup.py
diff options
context:
space:
mode:
Diffstat (limited to 'libs/rich/markup.py')
-rw-r--r--libs/rich/markup.py99
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)