Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
markup.py252 linesDownload Raw Back to rich
1import re2from ast import literal_eval3from operator import attrgetter4from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union5 6from ._emoji_replace import _emoji_replace7from .emoji import EmojiVariant8from .errors import MarkupError9from .style import Style10from .text import Span, Text11 12RE_TAGS = re.compile(13    r"""((\\*)\[([a-z#/@][^[]*?)])""",14    re.VERBOSE,15)16 17RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")18 19 20class Tag(NamedTuple):21    """A tag in console markup."""22 23    name: str24    """The tag name. e.g. 'bold'."""25    parameters: Optional[str]26    """Any additional parameters after the name."""27 28    def __str__(self) -> str:29        return (30            self.name if self.parameters is None else f"{self.name} {self.parameters}"31        )32 33    @property34    def markup(self) -> str:35        """Get the string representation of this tag."""36        return (37            f"[{self.name}]"38            if self.parameters is None39            else f"[{self.name}={self.parameters}]"40        )41 42 43_ReStringMatch = Match[str]  # regex match object44_ReSubCallable = Callable[[_ReStringMatch], str]  # Callable invoked by re.sub45_EscapeSubMethod = Callable[[_ReSubCallable, str], str]  # Sub method of a compiled re46 47 48def escape(49    markup: str,50    _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub,51) -> str:52    """Escapes text so that it won't be interpreted as markup.53 54    Args:55        markup (str): Content to be inserted in to markup.56 57    Returns:58        str: Markup with square brackets escaped.59    """60 61    def escape_backslashes(match: Match[str]) -> str:62        """Called by re.sub replace matches."""63        backslashes, text = match.groups()64        return f"{backslashes}{backslashes}\\{text}"65 66    markup = _escape(escape_backslashes, markup)67    if markup.endswith("\\") and not markup.endswith("\\\\"):68        return markup + "\\"69 70    return markup71 72 73def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:74    """Parse markup in to an iterable of tuples of (position, text, tag).75 76    Args:77        markup (str): A string containing console markup78 79    """80    position = 081    _divmod = divmod82    _Tag = Tag83    for match in RE_TAGS.finditer(markup):84        full_text, escapes, tag_text = match.groups()85        start, end = match.span()86        if start > position:87            yield start, markup[position:start], None88        if escapes:89            backslashes, escaped = _divmod(len(escapes), 2)90            if backslashes:91                # Literal backslashes92                yield start, "\\" * backslashes, None93                start += backslashes * 294            if escaped:95                # Escape of tag96                yield start, full_text[len(escapes) :], None97                position = end98                continue99        text, equals, parameters = tag_text.partition("=")100        yield start, None, _Tag(text, parameters if equals else None)101        position = end102    if position < len(markup):103        yield position, markup[position:], None104 105 106def render(107    markup: str,108    style: Union[str, Style] = "",109    emoji: bool = True,110    emoji_variant: Optional[EmojiVariant] = None,111) -> Text:112    """Render console markup in to a Text instance.113 114    Args:115        markup (str): A string containing console markup.116        style: (Union[str, Style]): The style to use.117        emoji (bool, optional): Also render emoji code. Defaults to True.118        emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.119 120 121    Raises:122        MarkupError: If there is a syntax error in the markup.123 124    Returns:125        Text: A test instance.126    """127    emoji_replace = _emoji_replace128    if "[" not in markup:129        return Text(130            emoji_replace(markup, default_variant=emoji_variant) if emoji else markup,131            style=style,132        )133    text = Text(style=style)134    append = text.append135    normalize = Style.normalize136 137    style_stack: List[Tuple[int, Tag]] = []138    pop = style_stack.pop139 140    spans: List[Span] = []141    append_span = spans.append142 143    _Span = Span144    _Tag = Tag145 146    def pop_style(style_name: str) -> Tuple[int, Tag]:147        """Pop tag matching given style name."""148        for index, (_, tag) in enumerate(reversed(style_stack), 1):149            if tag.name == style_name:150                return pop(-index)151        raise KeyError(style_name)152 153    for position, plain_text, tag in _parse(markup):154        if plain_text is not None:155            # Handle open brace escapes, where the brace is not part of a tag.156            plain_text = plain_text.replace("\\[", "[")157            append(emoji_replace(plain_text) if emoji else plain_text)158        elif tag is not None:159            if tag.name.startswith("/"):  # Closing tag160                style_name = tag.name[1:].strip()161 162                if style_name:  # explicit close163                    style_name = normalize(style_name)164                    try:165                        start, open_tag = pop_style(style_name)166                    except KeyError:167                        raise MarkupError(168                            f"closing tag '{tag.markup}' at position {position} doesn't match any open tag"169                        ) from None170                else:  # implicit close171                    try:172                        start, open_tag = pop()173                    except IndexError:174                        raise MarkupError(175                            f"closing tag '[/]' at position {position} has nothing to close"176                        ) from None177 178                if open_tag.name.startswith("@"):179                    if open_tag.parameters:180                        handler_name = ""181                        parameters = open_tag.parameters.strip()182                        handler_match = RE_HANDLER.match(parameters)183                        if handler_match is not None:184                            handler_name, match_parameters = handler_match.groups()185                            parameters = (186                                "()" if match_parameters is None else match_parameters187                            )188 189                        try:190                            meta_params = literal_eval(parameters)191                        except SyntaxError as error:192                            raise MarkupError(193                                f"error parsing {parameters!r} in {open_tag.parameters!r}; {error.msg}"194                            )195                        except Exception as error:196                            raise MarkupError(197                                f"error parsing {open_tag.parameters!r}; {error}"198                            ) from None199 200                        if handler_name:201                            meta_params = (202                                handler_name,203                                meta_params204                                if isinstance(meta_params, tuple)205                                else (meta_params,),206                            )207 208                    else:209                        meta_params = ()210 211                    append_span(212                        _Span(213                            start, len(text), Style(meta={open_tag.name: meta_params})214                        )215                    )216                else:217                    append_span(_Span(start, len(text), str(open_tag)))218 219            else:  # Opening tag220                normalized_tag = _Tag(normalize(tag.name), tag.parameters)221                style_stack.append((len(text), normalized_tag))222 223    text_length = len(text)224    while style_stack:225        start, tag = style_stack.pop()226        style = str(tag)227        if style:228            append_span(_Span(start, text_length, style))229 230    text.spans = sorted(spans[::-1], key=attrgetter("start"))231    return text232 233 234if __name__ == "__main__":  # pragma: no cover235    MARKUP = [236        "[red]Hello World[/red]",237        "[magenta]Hello [b]World[/b]",238        "[bold]Bold[italic] bold and italic [/bold]italic[/italic]",239        "Click [link=https://www.willmcgugan.com]here[/link] to visit my Blog",240        ":warning-emoji: [bold red blink] DANGER![/]",241    ]242 243    from rich import print244    from rich.table import Table245 246    grid = Table("Markup", "Result", padding=(0, 1))247 248    for markup in MARKUP:249        grid.add_row(Text(markup), markup)250 251    print(grid)252 
codekingpro/portable-devtools · Team Ai