Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
text.py1358 linesDownload Raw Back to rich
1import re2from functools import partial, reduce3from math import gcd4from operator import itemgetter5from typing import (6    TYPE_CHECKING,7    Any,8    Callable,9    Dict,10    Iterable,11    List,12    NamedTuple,13    Optional,14    Tuple,15    Union,16)17 18from ._loop import loop_last19from ._pick import pick_bool20from ._wrap import divide_line21from .align import AlignMethod22from .cells import cell_len, set_cell_size23from .containers import Lines24from .control import strip_control_codes25from .emoji import EmojiVariant26from .jupyter import JupyterMixin27from .measure import Measurement28from .segment import Segment29from .style import Style, StyleType30 31if TYPE_CHECKING:  # pragma: no cover32    from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod33 34DEFAULT_JUSTIFY: "JustifyMethod" = "default"35DEFAULT_OVERFLOW: "OverflowMethod" = "fold"36 37 38_re_whitespace = re.compile(r"\s+$")39 40TextType = Union[str, "Text"]41"""A plain string or a :class:`Text` instance."""42 43GetStyleCallable = Callable[[str], Optional[StyleType]]44 45 46class Span(NamedTuple):47    """A marked up region in some text."""48 49    start: int50    """Span start index."""51    end: int52    """Span end index."""53    style: Union[str, Style]54    """Style associated with the span."""55 56    def __repr__(self) -> str:57        return f"Span({self.start}, {self.end}, {self.style!r})"58 59    def __bool__(self) -> bool:60        return self.end > self.start61 62    def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:63        """Split a span in to 2 from a given offset."""64 65        if offset < self.start:66            return self, None67        if offset >= self.end:68            return self, None69 70        start, end, style = self71        span1 = Span(start, min(end, offset), style)72        span2 = Span(span1.end, end, style)73        return span1, span274 75    def move(self, offset: int) -> "Span":76        """Move start and end by a given offset.77 78        Args:79            offset (int): Number of characters to add to start and end.80 81        Returns:82            TextSpan: A new TextSpan with adjusted position.83        """84        start, end, style = self85        return Span(start + offset, end + offset, style)86 87    def right_crop(self, offset: int) -> "Span":88        """Crop the span at the given offset.89 90        Args:91            offset (int): A value between start and end.92 93        Returns:94            Span: A new (possibly smaller) span.95        """96        start, end, style = self97        if offset >= end:98            return self99        return Span(start, min(offset, end), style)100 101    def extend(self, cells: int) -> "Span":102        """Extend the span by the given number of cells.103 104        Args:105            cells (int): Additional space to add to end of span.106 107        Returns:108            Span: A span.109        """110        if cells:111            start, end, style = self112            return Span(start, end + cells, style)113        else:114            return self115 116 117class Text(JupyterMixin):118    """Text with color / style.119 120    Args:121        text (str, optional): Default unstyled text. Defaults to "".122        style (Union[str, Style], optional): Base style for text. Defaults to "".123        justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.124        overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.125        no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.126        end (str, optional): Character to end text with. Defaults to "\\\\n".127        tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.128        spans (List[Span], optional). A list of predefined style spans. Defaults to None.129    """130 131    __slots__ = [132        "_text",133        "style",134        "justify",135        "overflow",136        "no_wrap",137        "end",138        "tab_size",139        "_spans",140        "_length",141    ]142 143    def __init__(144        self,145        text: str = "",146        style: Union[str, Style] = "",147        *,148        justify: Optional["JustifyMethod"] = None,149        overflow: Optional["OverflowMethod"] = None,150        no_wrap: Optional[bool] = None,151        end: str = "\n",152        tab_size: Optional[int] = None,153        spans: Optional[List[Span]] = None,154    ) -> None:155        sanitized_text = strip_control_codes(text)156        self._text = [sanitized_text]157        self.style = style158        self.justify: Optional["JustifyMethod"] = justify159        self.overflow: Optional["OverflowMethod"] = overflow160        self.no_wrap = no_wrap161        self.end = end162        self.tab_size = tab_size163        self._spans: List[Span] = spans or []164        self._length: int = len(sanitized_text)165 166    def __len__(self) -> int:167        return self._length168 169    def __bool__(self) -> bool:170        return bool(self._length)171 172    def __str__(self) -> str:173        return self.plain174 175    def __repr__(self) -> str:176        return f"<text {self.plain!r} {self._spans!r}>"177 178    def __add__(self, other: Any) -> "Text":179        if isinstance(other, (str, Text)):180            result = self.copy()181            result.append(other)182            return result183        return NotImplemented184 185    def __eq__(self, other: object) -> bool:186        if not isinstance(other, Text):187            return NotImplemented188        return self.plain == other.plain and self._spans == other._spans189 190    def __contains__(self, other: object) -> bool:191        if isinstance(other, str):192            return other in self.plain193        elif isinstance(other, Text):194            return other.plain in self.plain195        return False196 197    def __getitem__(self, slice: Union[int, slice]) -> "Text":198        def get_text_at(offset: int) -> "Text":199            _Span = Span200            text = Text(201                self.plain[offset],202                spans=[203                    _Span(0, 1, style)204                    for start, end, style in self._spans205                    if end > offset >= start206                ],207                end="",208            )209            return text210 211        if isinstance(slice, int):212            return get_text_at(slice)213        else:214            start, stop, step = slice.indices(len(self.plain))215            if step == 1:216                lines = self.divide([start, stop])217                return lines[1]218            else:219                # This would be a bit of work to implement efficiently220                # For now, its not required221                raise TypeError("slices with step!=1 are not supported")222 223    @property224    def cell_len(self) -> int:225        """Get the number of cells required to render this text."""226        return cell_len(self.plain)227 228    @property229    def markup(self) -> str:230        """Get console markup to render this Text.231 232        Returns:233            str: A string potentially creating markup tags.234        """235        from .markup import escape236 237        output: List[str] = []238 239        plain = self.plain240        markup_spans = [241            (0, False, self.style),242            *((span.start, False, span.style) for span in self._spans),243            *((span.end, True, span.style) for span in self._spans),244            (len(plain), True, self.style),245        ]246        markup_spans.sort(key=itemgetter(0, 1))247        position = 0248        append = output.append249        for offset, closing, style in markup_spans:250            if offset > position:251                append(escape(plain[position:offset]))252                position = offset253            if style:254                append(f"[/{style}]" if closing else f"[{style}]")255        markup = "".join(output)256        return markup257 258    @classmethod259    def from_markup(260        cls,261        text: str,262        *,263        style: Union[str, Style] = "",264        emoji: bool = True,265        emoji_variant: Optional[EmojiVariant] = None,266        justify: Optional["JustifyMethod"] = None,267        overflow: Optional["OverflowMethod"] = None,268        end: str = "\n",269    ) -> "Text":270        """Create Text instance from markup.271 272        Args:273            text (str): A string containing console markup.274            style (Union[str, Style], optional): Base style for text. Defaults to "".275            emoji (bool, optional): Also render emoji code. Defaults to True.276            emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.277            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.278            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.279            end (str, optional): Character to end text with. Defaults to "\\\\n".280 281        Returns:282            Text: A Text instance with markup rendered.283        """284        from .markup import render285 286        rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant)287        rendered_text.justify = justify288        rendered_text.overflow = overflow289        rendered_text.end = end290        return rendered_text291 292    @classmethod293    def from_ansi(294        cls,295        text: str,296        *,297        style: Union[str, Style] = "",298        justify: Optional["JustifyMethod"] = None,299        overflow: Optional["OverflowMethod"] = None,300        no_wrap: Optional[bool] = None,301        end: str = "\n",302        tab_size: Optional[int] = 8,303    ) -> "Text":304        """Create a Text object from a string containing ANSI escape codes.305 306        Args:307            text (str): A string containing escape codes.308            style (Union[str, Style], optional): Base style for text. Defaults to "".309            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.310            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.311            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.312            end (str, optional): Character to end text with. Defaults to "\\\\n".313            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.314        """315        from .ansi import AnsiDecoder316 317        joiner = Text(318            "\n",319            justify=justify,320            overflow=overflow,321            no_wrap=no_wrap,322            end=end,323            tab_size=tab_size,324            style=style,325        )326        decoder = AnsiDecoder()327        result = joiner.join(line for line in decoder.decode(text))328        return result329 330    @classmethod331    def styled(332        cls,333        text: str,334        style: StyleType = "",335        *,336        justify: Optional["JustifyMethod"] = None,337        overflow: Optional["OverflowMethod"] = None,338    ) -> "Text":339        """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used340        to pad the text when it is justified.341 342        Args:343            text (str): A string containing console markup.344            style (Union[str, Style]): Style to apply to the text. Defaults to "".345            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.346            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.347 348        Returns:349            Text: A text instance with a style applied to the entire string.350        """351        styled_text = cls(text, justify=justify, overflow=overflow)352        styled_text.stylize(style)353        return styled_text354 355    @classmethod356    def assemble(357        cls,358        *parts: Union[str, "Text", Tuple[str, StyleType]],359        style: Union[str, Style] = "",360        justify: Optional["JustifyMethod"] = None,361        overflow: Optional["OverflowMethod"] = None,362        no_wrap: Optional[bool] = None,363        end: str = "\n",364        tab_size: int = 8,365        meta: Optional[Dict[str, Any]] = None,366    ) -> "Text":367        """Construct a text instance by combining a sequence of strings with optional styles.368        The positional arguments should be either strings, or a tuple of string + style.369 370        Args:371            style (Union[str, Style], optional): Base style for text. Defaults to "".372            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.373            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.374            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.375            end (str, optional): Character to end text with. Defaults to "\\\\n".376            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.377            meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None378 379        Returns:380            Text: A new text instance.381        """382        text = cls(383            style=style,384            justify=justify,385            overflow=overflow,386            no_wrap=no_wrap,387            end=end,388            tab_size=tab_size,389        )390        append = text.append391        _Text = Text392        for part in parts:393            if isinstance(part, (_Text, str)):394                append(part)395            else:396                append(*part)397        if meta:398            text.apply_meta(meta)399        return text400 401    @property402    def plain(self) -> str:403        """Get the text as a single string."""404        if len(self._text) != 1:405            self._text[:] = ["".join(self._text)]406        return self._text[0]407 408    @plain.setter409    def plain(self, new_text: str) -> None:410        """Set the text to a new value."""411        if new_text != self.plain:412            sanitized_text = strip_control_codes(new_text)413            self._text[:] = [sanitized_text]414            old_length = self._length415            self._length = len(sanitized_text)416            if old_length > self._length:417                self._trim_spans()418 419    @property420    def spans(self) -> List[Span]:421        """Get a reference to the internal list of spans."""422        return self._spans423 424    @spans.setter425    def spans(self, spans: List[Span]) -> None:426        """Set spans."""427        self._spans = spans[:]428 429    def blank_copy(self, plain: str = "") -> "Text":430        """Return a new Text instance with copied metadata (but not the string or spans)."""431        copy_self = Text(432            plain,433            style=self.style,434            justify=self.justify,435            overflow=self.overflow,436            no_wrap=self.no_wrap,437            end=self.end,438            tab_size=self.tab_size,439        )440        return copy_self441 442    def copy(self) -> "Text":443        """Return a copy of this instance."""444        copy_self = Text(445            self.plain,446            style=self.style,447            justify=self.justify,448            overflow=self.overflow,449            no_wrap=self.no_wrap,450            end=self.end,451            tab_size=self.tab_size,452        )453        copy_self._spans[:] = self._spans454        return copy_self455 456    def stylize(457        self,458        style: Union[str, Style],459        start: int = 0,460        end: Optional[int] = None,461    ) -> None:462        """Apply a style to the text, or a portion of the text.463 464        Args:465            style (Union[str, Style]): Style instance or style definition to apply.466            start (int): Start offset (negative indexing is supported). Defaults to 0.467            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.468        """469        if style:470            length = len(self)471            if start < 0:472                start = length + start473            if end is None:474                end = length475            if end < 0:476                end = length + end477            if start >= length or end <= start:478                # Span not in text or not valid479                return480            self._spans.append(Span(start, min(length, end), style))481 482    def stylize_before(483        self,484        style: Union[str, Style],485        start: int = 0,486        end: Optional[int] = None,487    ) -> None:488        """Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.489 490        Args:491            style (Union[str, Style]): Style instance or style definition to apply.492            start (int): Start offset (negative indexing is supported). Defaults to 0.493            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.494        """495        if style:496            length = len(self)497            if start < 0:498                start = length + start499            if end is None:500                end = length501            if end < 0:502                end = length + end503            if start >= length or end <= start:504                # Span not in text or not valid505                return506            self._spans.insert(0, Span(start, min(length, end), style))507 508    def apply_meta(509        self, meta: Dict[str, Any], start: int = 0, end: Optional[int] = None510    ) -> None:511        """Apply metadata to the text, or a portion of the text.512 513        Args:514            meta (Dict[str, Any]): A dict of meta information.515            start (int): Start offset (negative indexing is supported). Defaults to 0.516            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.517 518        """519        style = Style.from_meta(meta)520        self.stylize(style, start=start, end=end)521 522    def on(self, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Text":523        """Apply event handlers (used by Textual project).524 525        Example:526            >>> from rich.text import Text527            >>> text = Text("hello world")528            >>> text.on(click="view.toggle('world')")529 530        Args:531            meta (Dict[str, Any]): Mapping of meta information.532            **handlers: Keyword args are prefixed with "@" to defined handlers.533 534        Returns:535            Text: Self is returned to method may be chained.536        """537        meta = {} if meta is None else meta538        meta.update({f"@{key}": value for key, value in handlers.items()})539        self.stylize(Style.from_meta(meta))540        return self541 542    def remove_suffix(self, suffix: str) -> None:543        """Remove a suffix if it exists.544 545        Args:546            suffix (str): Suffix to remove.547        """548        if self.plain.endswith(suffix):549            self.right_crop(len(suffix))550 551    def get_style_at_offset(self, console: "Console", offset: int) -> Style:552        """Get the style of a character at give offset.553 554        Args:555            console (~Console): Console where text will be rendered.556            offset (int): Offset in to text (negative indexing supported)557 558        Returns:559            Style: A Style instance.560        """561        # TODO: This is a little inefficient, it is only used by full justify562        if offset < 0:563            offset = len(self) + offset564        get_style = console.get_style565        style = get_style(self.style).copy()566        for start, end, span_style in self._spans:567            if end > offset >= start:568                style += get_style(span_style, default="")569        return style570 571    def extend_style(self, spaces: int) -> None:572        """Extend the Text given number of spaces where the spaces have the same style as the last character.573 574        Args:575            spaces (int): Number of spaces to add to the Text.576        """577        if spaces <= 0:578            return579        spans = self.spans580        new_spaces = " " * spaces581        if spans:582            end_offset = len(self)583            self._spans[:] = [584                span.extend(spaces) if span.end >= end_offset else span585                for span in spans586            ]587            self._text.append(new_spaces)588            self._length += spaces589        else:590            self.plain += new_spaces591 592    def highlight_regex(593        self,594        re_highlight: str,595        style: Optional[Union[GetStyleCallable, StyleType]] = None,596        *,597        style_prefix: str = "",598    ) -> int:599        """Highlight text with a regular expression, where group names are600        translated to styles.601 602        Args:603            re_highlight (str): A regular expression.604            style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable605                which accepts the matched text and returns a style. Defaults to None.606            style_prefix (str, optional): Optional prefix to add to style group names.607 608        Returns:609            int: Number of regex matches610        """611        count = 0612        append_span = self._spans.append613        _Span = Span614        plain = self.plain615        for match in re.finditer(re_highlight, plain):616            get_span = match.span617            if style:618                start, end = get_span()619                match_style = style(plain[start:end]) if callable(style) else style620                if match_style is not None and end > start:621                    append_span(_Span(start, end, match_style))622 623            count += 1624            for name in match.groupdict().keys():625                start, end = get_span(name)626                if start != -1 and end > start:627                    append_span(_Span(start, end, f"{style_prefix}{name}"))628        return count629 630    def highlight_words(631        self,632        words: Iterable[str],633        style: Union[str, Style],634        *,635        case_sensitive: bool = True,636    ) -> int:637        """Highlight words with a style.638 639        Args:640            words (Iterable[str]): Words to highlight.641            style (Union[str, Style]): Style to apply.642            case_sensitive (bool, optional): Enable case sensitive matching. Defaults to True.643 644        Returns:645            int: Number of words highlighted.646        """647        re_words = "|".join(re.escape(word) for word in words)648        add_span = self._spans.append649        count = 0650        _Span = Span651        for match in re.finditer(652            re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE653        ):654            start, end = match.span(0)655            add_span(_Span(start, end, style))656            count += 1657        return count658 659    def rstrip(self) -> None:660        """Strip whitespace from end of text."""661        self.plain = self.plain.rstrip()662 663    def rstrip_end(self, size: int) -> None:664        """Remove whitespace beyond a certain width at the end of the text.665 666        Args:667            size (int): The desired size of the text.668        """669        text_length = len(self)670        if text_length > size:671            excess = text_length - size672            whitespace_match = _re_whitespace.search(self.plain)673            if whitespace_match is not None:674                whitespace_count = len(whitespace_match.group(0))675                self.right_crop(min(whitespace_count, excess))676 677    def set_length(self, new_length: int) -> None:678        """Set new length of the text, clipping or padding is required."""679        length = len(self)680        if length != new_length:681            if length < new_length:682                self.pad_right(new_length - length)683            else:684                self.right_crop(length - new_length)685 686    def __rich_console__(687        self, console: "Console", options: "ConsoleOptions"688    ) -> Iterable[Segment]:689        tab_size: int = console.tab_size if self.tab_size is None else self.tab_size690        justify = self.justify or options.justify or DEFAULT_JUSTIFY691 692        overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW693 694        lines = self.wrap(695            console,696            options.max_width,697            justify=justify,698            overflow=overflow,699            tab_size=tab_size or 8,700            no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),701        )702        all_lines = Text("\n").join(lines)703        yield from all_lines.render(console, end=self.end)704 705    def __rich_measure__(706        self, console: "Console", options: "ConsoleOptions"707    ) -> Measurement:708        text = self.plain709        lines = text.splitlines()710        max_text_width = max(cell_len(line) for line in lines) if lines else 0711        words = text.split()712        min_text_width = (713            max(cell_len(word) for word in words) if words else max_text_width714        )715        return Measurement(min_text_width, max_text_width)716 717    def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:718        """Render the text as Segments.719 720        Args:721            console (Console): Console instance.722            end (Optional[str], optional): Optional end character.723 724        Returns:725            Iterable[Segment]: Result of render that may be written to the console.726        """727        _Segment = Segment728        text = self.plain729        if not self._spans:730            yield Segment(text)731            if end:732                yield _Segment(end)733            return734        get_style = partial(console.get_style, default=Style.null())735 736        enumerated_spans = list(enumerate(self._spans, 1))737        style_map = {index: get_style(span.style) for index, span in enumerated_spans}738        style_map[0] = get_style(self.style)739 740        spans = [741            (0, False, 0),742            *((span.start, False, index) for index, span in enumerated_spans),743            *((span.end, True, index) for index, span in enumerated_spans),744            (len(text), True, 0),745        ]746        spans.sort(key=itemgetter(0, 1))747 748        stack: List[int] = []749        stack_append = stack.append750        stack_pop = stack.remove751 752        style_cache: Dict[Tuple[Style, ...], Style] = {}753        style_cache_get = style_cache.get754        combine = Style.combine755 756        def get_current_style() -> Style:757            """Construct current style from stack."""758            styles = tuple(style_map[_style_id] for _style_id in sorted(stack))759            cached_style = style_cache_get(styles)760            if cached_style is not None:761                return cached_style762            current_style = combine(styles)763            style_cache[styles] = current_style764            return current_style765 766        for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):767            if leaving:768                stack_pop(style_id)769            else:770                stack_append(style_id)771            if next_offset > offset:772                yield _Segment(text[offset:next_offset], get_current_style())773        if end:774            yield _Segment(end)775 776    def join(self, lines: Iterable["Text"]) -> "Text":777        """Join text together with this instance as the separator.778 779        Args:780            lines (Iterable[Text]): An iterable of Text instances to join.781 782        Returns:783            Text: A new text instance containing join text.784        """785 786        new_text = self.blank_copy()787 788        def iter_text() -> Iterable["Text"]:789            if self.plain:790                for last, line in loop_last(lines):791                    yield line792                    if not last:793                        yield self794            else:795                yield from lines796 797        extend_text = new_text._text.extend798        append_span = new_text._spans.append799        extend_spans = new_text._spans.extend800        offset = 0801        _Span = Span802 803        for text in iter_text():804            extend_text(text._text)805            if text.style:806                append_span(_Span(offset, offset + len(text), text.style))807            extend_spans(808                _Span(offset + start, offset + end, style)809                for start, end, style in text._spans810            )811            offset += len(text)812        new_text._length = offset813        return new_text814 815    def expand_tabs(self, tab_size: Optional[int] = None) -> None:816        """Converts tabs to spaces.817 818        Args:819            tab_size (int, optional): Size of tabs. Defaults to 8.820 821        """822        if "\t" not in self.plain:823            return824        if tab_size is None:825            tab_size = self.tab_size826        if tab_size is None:827            tab_size = 8828 829        new_text: List[Text] = []830        append = new_text.append831 832        for line in self.split("\n", include_separator=True):833            if "\t" not in line.plain:834                append(line)835            else:836                cell_position = 0837                parts = line.split("\t", include_separator=True)838                for part in parts:839                    if part.plain.endswith("\t"):840                        part._text[-1] = part._text[-1][:-1] + " "841                        cell_position += part.cell_len842                        tab_remainder = cell_position % tab_size843                        if tab_remainder:844                            spaces = tab_size - tab_remainder845                            part.extend_style(spaces)846                            cell_position += spaces847                    else:848                        cell_position += part.cell_len849                    append(part)850 851        result = Text("").join(new_text)852 853        self._text = [result.plain]854        self._length = len(self.plain)855        self._spans[:] = result._spans856 857    def truncate(858        self,859        max_width: int,860        *,861        overflow: Optional["OverflowMethod"] = None,862        pad: bool = False,863    ) -> None:864        """Truncate text if it is longer that a given width.865 866        Args:867            max_width (int): Maximum number of characters in text.868            overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.869            pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.870        """871        _overflow = overflow or self.overflow or DEFAULT_OVERFLOW872        if _overflow != "ignore":873            length = cell_len(self.plain)874            if length > max_width:875                if _overflow == "ellipsis":876                    self.plain = set_cell_size(self.plain, max_width - 1) + "…"877                else:878                    self.plain = set_cell_size(self.plain, max_width)879            if pad and length < max_width:880                spaces = max_width - length881                self._text = [f"{self.plain}{' ' * spaces}"]882                self._length = len(self.plain)883 884    def _trim_spans(self) -> None:885        """Remove or modify any spans that are over the end of the text."""886        max_offset = len(self.plain)887        _Span = Span888        self._spans[:] = [889            (890                span891                if span.end < max_offset892                else _Span(span.start, min(max_offset, span.end), span.style)893            )894            for span in self._spans895            if span.start < max_offset896        ]897 898    def pad(self, count: int, character: str = " ") -> None:899        """Pad left and right with a given number of characters.900 901        Args:902            count (int): Width of padding.903            character (str): The character to pad with. Must be a string of length 1.904        """905        assert len(character) == 1, "Character must be a string of length 1"906        if count:907            pad_characters = character * count908            self.plain = f"{pad_characters}{self.plain}{pad_characters}"909            _Span = Span910            self._spans[:] = [911                _Span(start + count, end + count, style)912                for start, end, style in self._spans913            ]914 915    def pad_left(self, count: int, character: str = " ") -> None:916        """Pad the left with a given character.917 918        Args:919            count (int): Number of characters to pad.920            character (str, optional): Character to pad with. Defaults to " ".921        """922        assert len(character) == 1, "Character must be a string of length 1"923        if count:924            self.plain = f"{character * count}{self.plain}"925            _Span = Span926            self._spans[:] = [927                _Span(start + count, end + count, style)928                for start, end, style in self._spans929            ]930 931    def pad_right(self, count: int, character: str = " ") -> None:932        """Pad the right with a given character.933 934        Args:935            count (int): Number of characters to pad.936            character (str, optional): Character to pad with. Defaults to " ".937        """938        assert len(character) == 1, "Character must be a string of length 1"939        if count:940            self.plain = f"{self.plain}{character * count}"941 942    def align(self, align: AlignMethod, width: int, character: str = " ") -> None:943        """Align text to a given width.944 945        Args:946            align (AlignMethod): One of "left", "center", or "right".947            width (int): Desired width.948            character (str, optional): Character to pad with. Defaults to " ".949        """950        self.truncate(width)951        excess_space = width - cell_len(self.plain)952        if excess_space:953            if align == "left":954                self.pad_right(excess_space, character)955            elif align == "center":956                left = excess_space // 2957                self.pad_left(left, character)958                self.pad_right(excess_space - left, character)959            else:960                self.pad_left(excess_space, character)961 962    def append(963        self, text: Union["Text", str], style: Optional[Union[str, "Style"]] = None964    ) -> "Text":965        """Add text with an optional style.966 967        Args:968            text (Union[Text, str]): A str or Text to append.969            style (str, optional): A style name. Defaults to None.970 971        Returns:972            Text: Returns self for chaining.973        """974 975        if not isinstance(text, (str, Text)):976            raise TypeError("Only str or Text can be appended to Text")977 978        if len(text):979            if isinstance(text, str):980                sanitized_text = strip_control_codes(text)981                self._text.append(sanitized_text)982                offset = len(self)983                text_length = len(sanitized_text)984                if style:985                    self._spans.append(Span(offset, offset + text_length, style))986                self._length += text_length987            elif isinstance(text, Text):988                _Span = Span989                if style is not None:990                    raise ValueError(991                        "style must not be set when appending Text instance"992                    )993                text_length = self._length994                if text.style:995                    self._spans.append(996                        _Span(text_length, text_length + len(text), text.style)997                    )998                self._text.append(text.plain)999                self._spans.extend(1000                    _Span(start + text_length, end + text_length, style)1001                    for start, end, style in text._spans1002                )1003                self._length += len(text)1004        return self1005 1006    def append_text(self, text: "Text") -> "Text":1007        """Append another Text instance. This method is more performant that Text.append, but1008        only works for Text.1009 1010        Args:1011            text (Text): The Text instance to append to this instance.1012 1013        Returns:1014            Text: Returns self for chaining.1015        """1016        _Span = Span1017        text_length = self._length1018        if text.style:1019            self._spans.append(_Span(text_length, text_length + len(text), text.style))1020        self._text.append(text.plain)1021        self._spans.extend(1022            _Span(start + text_length, end + text_length, style)1023            for start, end, style in text._spans1024        )1025        self._length += len(text)1026        return self1027 1028    def append_tokens(1029        self, tokens: Iterable[Tuple[str, Optional[StyleType]]]1030    ) -> "Text":1031        """Append iterable of str and style. Style may be a Style instance or a str style definition.1032 1033        Args:1034            tokens (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.1035 1036        Returns:1037            Text: Returns self for chaining.1038        """1039        append_text = self._text.append1040        append_span = self._spans.append1041        _Span = Span1042        offset = len(self)1043        for content, style in tokens:1044            append_text(content)1045            if style:1046                append_span(_Span(offset, offset + len(content), style))1047            offset += len(content)1048        self._length = offset1049        return self1050 1051    def copy_styles(self, text: "Text") -> None:1052        """Copy styles from another Text instance.1053 1054        Args:1055            text (Text): A Text instance to copy styles from, must be the same length.1056        """1057        self._spans.extend(text._spans)1058 1059    def split(1060        self,1061        separator: str = "\n",1062        *,1063        include_separator: bool = False,1064        allow_blank: bool = False,1065    ) -> Lines:1066        """Split rich text in to lines, preserving styles.1067 1068        Args:1069            separator (str, optional): String to split on. Defaults to "\\\\n".1070            include_separator (bool, optional): Include the separator in the lines. Defaults to False.1071            allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.1072 1073        Returns:1074            List[RichText]: A list of rich text, one per line of the original.1075        """1076        assert separator, "separator must not be empty"1077 1078        text = self.plain1079        if separator not in text:1080            return Lines([self.copy()])1081 1082        if include_separator:1083            lines = self.divide(1084                match.end() for match in re.finditer(re.escape(separator), text)1085            )1086        else:1087 1088            def flatten_spans() -> Iterable[int]:1089                for match in re.finditer(re.escape(separator), text):1090                    start, end = match.span()1091                    yield start1092                    yield end1093 1094            lines = Lines(1095                line for line in self.divide(flatten_spans()) if line.plain != separator1096            )1097 1098        if not allow_blank and text.endswith(separator):1099            lines.pop()1100 1101        return lines1102 1103    def divide(self, offsets: Iterable[int]) -> Lines:1104        """Divide text in to a number of lines at given offsets.1105 1106        Args:1107            offsets (Iterable[int]): Offsets used to divide text.1108 1109        Returns:1110            Lines: New RichText instances between offsets.1111        """1112        _offsets = list(offsets)1113 1114        if not _offsets:1115            return Lines([self.copy()])1116 1117        text = self.plain1118        text_length = len(text)1119        divide_offsets = [0, *_offsets, text_length]1120        line_ranges = list(zip(divide_offsets, divide_offsets[1:]))1121 1122        style = self.style1123        justify = self.justify1124        overflow = self.overflow1125        _Text = Text1126        new_lines = Lines(1127            _Text(1128                text[start:end],1129                style=style,1130                justify=justify,1131                overflow=overflow,1132            )1133            for start, end in line_ranges1134        )1135        if not self._spans:1136            return new_lines1137 1138        _line_appends = [line._spans.append for line in new_lines._lines]1139        line_count = len(line_ranges)1140        _Span = Span1141 1142        for span_start, span_end, style in self._spans:1143            lower_bound = 01144            upper_bound = line_count1145            start_line_no = (lower_bound + upper_bound) // 21146 1147            while True:1148                line_start, line_end = line_ranges[start_line_no]1149                if span_start < line_start:1150                    upper_bound = start_line_no - 11151                elif span_start > line_end:1152                    lower_bound = start_line_no + 11153                else:1154                    break1155                start_line_no = (lower_bound + upper_bound) // 21156 1157            if span_end < line_end:1158                end_line_no = start_line_no1159            else:1160                end_line_no = lower_bound = start_line_no1161                upper_bound = line_count1162 1163                while True:1164                    line_start, line_end = line_ranges[end_line_no]1165                    if span_end < line_start:1166                        upper_bound = end_line_no - 11167                    elif span_end > line_end:1168                        lower_bound = end_line_no + 11169                    else:1170                        break1171                    end_line_no = (lower_bound + upper_bound) // 21172 1173            for line_no in range(start_line_no, end_line_no + 1):1174                line_start, line_end = line_ranges[line_no]1175                new_start = max(0, span_start - line_start)1176                new_end = min(span_end - line_start, line_end - line_start)1177                if new_end > new_start:1178                    _line_appends[line_no](_Span(new_start, new_end, style))1179 1180        return new_lines1181 1182    def right_crop(self, amount: int = 1) -> None:1183        """Remove a number of characters from the end of the text."""1184        max_offset = len(self.plain) - amount1185        _Span = Span1186        self._spans[:] = [1187            (1188                span1189                if span.end < max_offset1190                else _Span(span.start, min(max_offset, span.end), span.style)1191            )1192            for span in self._spans1193            if span.start < max_offset1194        ]1195        self._text = [self.plain[:-amount]]1196        self._length -= amount1197 1198    def wrap(1199        self,1200        console: "Console",

Showing the first 1,200 of 1358 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai