diff --git a/README.md b/README.md index 1d2c944c..ad1e3a79 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,6 @@ console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:")
😃 🧛 💩 👍 🦝 
 
-
+ Please use this feature wisely. diff --git a/imgs/hello_world.png b/imgs/hello_world.png new file mode 100644 index 00000000..51851bee Binary files /dev/null and b/imgs/hello_world.png differ diff --git a/rich/_cache.py b/rich/_cache.py new file mode 100644 index 00000000..09b1bbbb --- /dev/null +++ b/rich/_cache.py @@ -0,0 +1,33 @@ +from collections import OrderedDict +from typing import Generic, TypeVar + + +CacheType = TypeVar("CacheType") + + +class LRUCache(Generic[CacheType], OrderedDict): + """ + A dictionary-like container that stores a given maximum items. + + If an additional item is added when the LRUCache is full, the least + recently used key is discarded to make room for the new item. + + """ + + def __init__(self, cache_size: int) -> None: + self.cache_size = cache_size + super(LRUCache, self).__init__() + + def __setitem__(self, key: bytes, value: CacheType) -> None: + """Store a new views, potentially discarding an old value.""" + if key not in self: + if len(self) >= self.cache_size: + self.popitem(last=False) + OrderedDict.__setitem__(self, key, value) + + def __getitem__(self, key: bytes) -> CacheType: + """Gets the item, but also makes it most recent.""" + value: CacheType = OrderedDict.__getitem__(self, key) + OrderedDict.__delitem__(self, key) + OrderedDict.__setitem__(self, key, value) + return value diff --git a/rich/markup.py b/rich/markup.py new file mode 100644 index 00000000..7b9980ac --- /dev/null +++ b/rich/markup.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections import defaultdict +from operator import itemgetter +import re +from typing import Dict, Iterable, List, Optional, Tuple + +from .errors import MarkupError +from .text import Span, Text + + +re_tags = re.compile(r"(\[\".*?\"\])|(\[.*?\])") + + +def _parse(markup: str) -> Iterable[Tuple[Optional[str], Optional[str]]]: + """Parse markup in to an iterable of pairs of text, tag. + + Args: + markup (str): A string containing console markup + + """ + position = 0 + for match in re_tags.finditer(markup): + escaped_text, tag_text = match.groups() + + start, end = match.span() + if start > position: + yield markup[position:start], None + if tag_text is not None: + yield None, tag_text + else: + yield escaped_text[2:-2], None # type: ignore + + position = end + if position < len(markup): + yield markup[position:], None + + +def render(markup: str) -> Text: + """Render console markup in to a Text instance. + + Args: + markup (str): A string containing console markup. + + Raises: + MarkupError: If there is a syntax error in the markup. + + Returns: + Text: A test instance. + """ + text = Text() + stylize = text.stylize + + styles: Dict[str, List[int]] = defaultdict(list) + style_stack: List[str] = [] + + for plain_text, tag in _parse(markup): + if plain_text is not None: + text.append(plain_text) + if tag is not None: + if tag.startswith("[/"): + style_name = tag[2:-1].strip() + try: + style_position = styles[style_name].pop() + except (KeyError, IndexError): + raise MarkupError( + f"closing tag {tag!r} at position {len(text)} doesn't match open tag" + ) + style_stack.remove(style_name) + stylize(style_position, len(text), style_name) + else: + style_name = tag[1:-1].strip() + styles[style_name].append(len(text)) + style_stack.append(style_name) + + text_length = len(text) + while style_stack: + style_name = style_stack.pop() + style_position = styles[style_name].pop() + text.stylize(style_position, text_length, style_name) + + return text + + +if __name__ == "__main__": + text = """Hello [bold red]World[/bold red]!""" + + from .console import Console + + console = Console() + console.print(render(text)) + console.print(len(render(text))) + console.print(len(render(text).text))