Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
text.py1364 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    Pattern,15    Tuple,16    Union,17)18 19from ._loop import loop_last20from ._pick import pick_bool21from ._wrap import divide_line22from .align import AlignMethod23from .cells import cell_len, set_cell_size24from .containers import Lines25from .control import strip_control_codes26from .emoji import EmojiVariant27from .jupyter import JupyterMixin28from .measure import Measurement29from .segment import Segment30from .style import Style, StyleType31 32if TYPE_CHECKING:  # pragma: no cover33    from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod34 35DEFAULT_JUSTIFY: "JustifyMethod" = "default"36DEFAULT_OVERFLOW: "OverflowMethod" = "fold"37 38 39_re_whitespace = re.compile(r"\s+$")40 41TextType = Union[str, "Text"]42"""A plain string or a :class:`Text` instance."""43 44GetStyleCallable = Callable[[str], Optional[StyleType]]45 46 47class Span(NamedTuple):48    """A marked up region in some text."""49 50    start: int51    """Span start index."""52    end: int53    """Span end index."""54    style: Union[str, Style]55    """Style associated with the span."""56 57    def __repr__(self) -> str:58        return f"Span({self.start}, {self.end}, {self.style!r})"59 60    def __bool__(self) -> bool:61        return self.end > self.start62 63    def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:64        """Split a span in to 2 from a given offset."""65 66        if offset < self.start:67            return self, None68        if offset >= self.end:69            return self, None70 71        start, end, style = self72        span1 = Span(start, min(end, offset), style)73        span2 = Span(span1.end, end, style)74        return span1, span275 76    def move(self, offset: int) -> "Span":77        """Move start and end by a given offset.78 79        Args:80            offset (int): Number of characters to add to start and end.81 82        Returns:83            TextSpan: A new TextSpan with adjusted position.84        """85        start, end, style = self86        return Span(start + offset, end + offset, style)87 88    def right_crop(self, offset: int) -> "Span":89        """Crop the span at the given offset.90 91        Args:92            offset (int): A value between start and end.93 94        Returns:95            Span: A new (possibly smaller) span.96        """97        start, end, style = self98        if offset >= end:99            return self100        return Span(start, min(offset, end), style)101 102    def extend(self, cells: int) -> "Span":103        """Extend the span by the given number of cells.104 105        Args:106            cells (int): Additional space to add to end of span.107 108        Returns:109            Span: A span.110        """111        if cells:112            start, end, style = self113            return Span(start, end + cells, style)114        else:115            return self116 117 118class Text(JupyterMixin):119    """Text with color / style.120 121    Args:122        text (str, optional): Default unstyled text. Defaults to "".123        style (Union[str, Style], optional): Base style for text. Defaults to "".124        justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.125        overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.126        no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.127        end (str, optional): Character to end text with. Defaults to "\\\\n".128        tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.129        spans (List[Span], optional). A list of predefined style spans. Defaults to None.130    """131 132    __slots__ = [133        "_text",134        "style",135        "justify",136        "overflow",137        "no_wrap",138        "end",139        "tab_size",140        "_spans",141        "_length",142    ]143 144    def __init__(145        self,146        text: str = "",147        style: Union[str, Style] = "",148        *,149        justify: Optional["JustifyMethod"] = None,150        overflow: Optional["OverflowMethod"] = None,151        no_wrap: Optional[bool] = None,152        end: str = "\n",153        tab_size: Optional[int] = None,154        spans: Optional[List[Span]] = None,155    ) -> None:156        sanitized_text = strip_control_codes(text)157        self._text = [sanitized_text]158        self.style = style159        self.justify: Optional["JustifyMethod"] = justify160        self.overflow: Optional["OverflowMethod"] = overflow161        self.no_wrap = no_wrap162        self.end = end163        self.tab_size = tab_size164        self._spans: List[Span] = spans or []165        self._length: int = len(sanitized_text)166 167    def __len__(self) -> int:168        return self._length169 170    def __bool__(self) -> bool:171        return bool(self._length)172 173    def __str__(self) -> str:174        return self.plain175 176    def __repr__(self) -> str:177        return f"<text {self.plain!r} {self._spans!r} {self.style!r}>"178 179    def __add__(self, other: Any) -> "Text":180        if isinstance(other, (str, Text)):181            result = self.copy()182            result.append(other)183            return result184        return NotImplemented185 186    def __eq__(self, other: object) -> bool:187        if not isinstance(other, Text):188            return NotImplemented189        return self.plain == other.plain and self._spans == other._spans190 191    def __contains__(self, other: object) -> bool:192        if isinstance(other, str):193            return other in self.plain194        elif isinstance(other, Text):195            return other.plain in self.plain196        return False197 198    def __getitem__(self, slice: Union[int, slice]) -> "Text":199        def get_text_at(offset: int) -> "Text":200            _Span = Span201            text = Text(202                self.plain[offset],203                spans=[204                    _Span(0, 1, style)205                    for start, end, style in self._spans206                    if end > offset >= start207                ],208                end="",209            )210            return text211 212        if isinstance(slice, int):213            return get_text_at(slice)214        else:215            start, stop, step = slice.indices(len(self.plain))216            if step == 1:217                lines = self.divide([start, stop])218                return lines[1]219            else:220                # This would be a bit of work to implement efficiently221                # For now, its not required222                raise TypeError("slices with step!=1 are not supported")223 224    @property225    def cell_len(self) -> int:226        """Get the number of cells required to render this text."""227        return cell_len(self.plain)228 229    @property230    def markup(self) -> str:231        """Get console markup to render this Text.232 233        Returns:234            str: A string potentially creating markup tags.235        """236        from .markup import escape237 238        output: List[str] = []239 240        plain = self.plain241        markup_spans = [242            (0, False, self.style),243            *((span.start, False, span.style) for span in self._spans),244            *((span.end, True, span.style) for span in self._spans),245            (len(plain), True, self.style),246        ]247        markup_spans.sort(key=itemgetter(0, 1))248        position = 0249        append = output.append250        for offset, closing, style in markup_spans:251            if offset > position:252                append(escape(plain[position:offset]))253                position = offset254            if style:255                append(f"[/{style}]" if closing else f"[{style}]")256        markup = "".join(output)257        return markup258 259    @classmethod260    def from_markup(261        cls,262        text: str,263        *,264        style: Union[str, Style] = "",265        emoji: bool = True,266        emoji_variant: Optional[EmojiVariant] = None,267        justify: Optional["JustifyMethod"] = None,268        overflow: Optional["OverflowMethod"] = None,269        end: str = "\n",270    ) -> "Text":271        """Create Text instance from markup.272 273        Args:274            text (str): A string containing console markup.275            style (Union[str, Style], optional): Base style for text. Defaults to "".276            emoji (bool, optional): Also render emoji code. Defaults to True.277            emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.278            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.279            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.280            end (str, optional): Character to end text with. Defaults to "\\\\n".281 282        Returns:283            Text: A Text instance with markup rendered.284        """285        from .markup import render286 287        rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant)288        rendered_text.justify = justify289        rendered_text.overflow = overflow290        rendered_text.end = end291        return rendered_text292 293    @classmethod294    def from_ansi(295        cls,296        text: str,297        *,298        style: Union[str, Style] = "",299        justify: Optional["JustifyMethod"] = None,300        overflow: Optional["OverflowMethod"] = None,301        no_wrap: Optional[bool] = None,302        end: str = "\n",303        tab_size: Optional[int] = 8,304    ) -> "Text":305        """Create a Text object from a string containing ANSI escape codes.306 307        Args:308            text (str): A string containing escape codes.309            style (Union[str, Style], optional): Base style for text. Defaults to "".310            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.311            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.312            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.313            end (str, optional): Character to end text with. Defaults to "\\\\n".314            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.315        """316        from .ansi import AnsiDecoder317 318        joiner = Text(319            "\n",320            justify=justify,321            overflow=overflow,322            no_wrap=no_wrap,323            end=end,324            tab_size=tab_size,325            style=style,326        )327        decoder = AnsiDecoder()328        result = joiner.join(line for line in decoder.decode(text))329        return result330 331    @classmethod332    def styled(333        cls,334        text: str,335        style: StyleType = "",336        *,337        justify: Optional["JustifyMethod"] = None,338        overflow: Optional["OverflowMethod"] = None,339    ) -> "Text":340        """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used341        to pad the text when it is justified.342 343        Args:344            text (str): A string containing console markup.345            style (Union[str, Style]): Style to apply to the text. Defaults to "".346            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.347            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.348 349        Returns:350            Text: A text instance with a style applied to the entire string.351        """352        styled_text = cls(text, justify=justify, overflow=overflow)353        styled_text.stylize(style)354        return styled_text355 356    @classmethod357    def assemble(358        cls,359        *parts: Union[str, "Text", Tuple[str, StyleType]],360        style: Union[str, Style] = "",361        justify: Optional["JustifyMethod"] = None,362        overflow: Optional["OverflowMethod"] = None,363        no_wrap: Optional[bool] = None,364        end: str = "\n",365        tab_size: int = 8,366        meta: Optional[Dict[str, Any]] = None,367    ) -> "Text":368        """Construct a text instance by combining a sequence of strings with optional styles.369        The positional arguments should be either strings, or a tuple of string + style.370 371        Args:372            style (Union[str, Style], optional): Base style for text. Defaults to "".373            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.374            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.375            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.376            end (str, optional): Character to end text with. Defaults to "\\\\n".377            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.378            meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None379 380        Returns:381            Text: A new text instance.382        """383        text = cls(384            style=style,385            justify=justify,386            overflow=overflow,387            no_wrap=no_wrap,388            end=end,389            tab_size=tab_size,390        )391        append = text.append392        _Text = Text393        for part in parts:394            if isinstance(part, (_Text, str)):395                append(part)396            else:397                append(*part)398        if meta:399            text.apply_meta(meta)400        return text401 402    @property403    def plain(self) -> str:404        """Get the text as a single string."""405        if len(self._text) != 1:406            self._text[:] = ["".join(self._text)]407        return self._text[0]408 409    @plain.setter410    def plain(self, new_text: str) -> None:411        """Set the text to a new value."""412        if new_text != self.plain:413            sanitized_text = strip_control_codes(new_text)414            self._text[:] = [sanitized_text]415            old_length = self._length416            self._length = len(sanitized_text)417            if old_length > self._length:418                self._trim_spans()419 420    @property421    def spans(self) -> List[Span]:422        """Get a reference to the internal list of spans."""423        return self._spans424 425    @spans.setter426    def spans(self, spans: List[Span]) -> None:427        """Set spans."""428        self._spans = spans[:]429 430    def blank_copy(self, plain: str = "") -> "Text":431        """Return a new Text instance with copied metadata (but not the string or spans)."""432        copy_self = Text(433            plain,434            style=self.style,435            justify=self.justify,436            overflow=self.overflow,437            no_wrap=self.no_wrap,438            end=self.end,439            tab_size=self.tab_size,440        )441        return copy_self442 443    def copy(self) -> "Text":444        """Return a copy of this instance."""445        copy_self = Text(446            self.plain,447            style=self.style,448            justify=self.justify,449            overflow=self.overflow,450            no_wrap=self.no_wrap,451            end=self.end,452            tab_size=self.tab_size,453        )454        copy_self._spans[:] = self._spans455        return copy_self456 457    def stylize(458        self,459        style: Union[str, Style],460        start: int = 0,461        end: Optional[int] = None,462    ) -> None:463        """Apply a style to the text, or a portion of the text.464 465        Args:466            style (Union[str, Style]): Style instance or style definition to apply.467            start (int): Start offset (negative indexing is supported). Defaults to 0.468            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.469        """470        if style:471            length = len(self)472            if start < 0:473                start = length + start474            if end is None:475                end = length476            if end < 0:477                end = length + end478            if start >= length or end <= start:479                # Span not in text or not valid480                return481            self._spans.append(Span(start, min(length, end), style))482 483    def stylize_before(484        self,485        style: Union[str, Style],486        start: int = 0,487        end: Optional[int] = None,488    ) -> None:489        """Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.490 491        Args:492            style (Union[str, Style]): Style instance or style definition to apply.493            start (int): Start offset (negative indexing is supported). Defaults to 0.494            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.495        """496        if style:497            length = len(self)498            if start < 0:499                start = length + start500            if end is None:501                end = length502            if end < 0:503                end = length + end504            if start >= length or end <= start:505                # Span not in text or not valid506                return507            self._spans.insert(0, Span(start, min(length, end), style))508 509    def apply_meta(510        self, meta: Dict[str, Any], start: int = 0, end: Optional[int] = None511    ) -> None:512        """Apply metadata to the text, or a portion of the text.513 514        Args:515            meta (Dict[str, Any]): A dict of meta information.516            start (int): Start offset (negative indexing is supported). Defaults to 0.517            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.518 519        """520        style = Style.from_meta(meta)521        self.stylize(style, start=start, end=end)522 523    def on(self, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Text":524        """Apply event handlers (used by Textual project).525 526        Example:527            >>> from rich.text import Text528            >>> text = Text("hello world")529            >>> text.on(click="view.toggle('world')")530 531        Args:532            meta (Dict[str, Any]): Mapping of meta information.533            **handlers: Keyword args are prefixed with "@" to defined handlers.534 535        Returns:536            Text: Self is returned to method may be chained.537        """538        meta = {} if meta is None else meta539        meta.update({f"@{key}": value for key, value in handlers.items()})540        self.stylize(Style.from_meta(meta))541        return self542 543    def remove_suffix(self, suffix: str) -> None:544        """Remove a suffix if it exists.545 546        Args:547            suffix (str): Suffix to remove.548        """549        if self.plain.endswith(suffix):550            self.right_crop(len(suffix))551 552    def get_style_at_offset(self, console: "Console", offset: int) -> Style:553        """Get the style of a character at give offset.554 555        Args:556            console (~Console): Console where text will be rendered.557            offset (int): Offset in to text (negative indexing supported)558 559        Returns:560            Style: A Style instance.561        """562        # TODO: This is a little inefficient, it is only used by full justify563        if offset < 0:564            offset = len(self) + offset565        get_style = console.get_style566        style = get_style(self.style).copy()567        for start, end, span_style in self._spans:568            if end > offset >= start:569                style += get_style(span_style, default="")570        return style571 572    def extend_style(self, spaces: int) -> None:573        """Extend the Text given number of spaces where the spaces have the same style as the last character.574 575        Args:576            spaces (int): Number of spaces to add to the Text.577        """578        if spaces <= 0:579            return580        spans = self.spans581        new_spaces = " " * spaces582        if spans:583            end_offset = len(self)584            self._spans[:] = [585                span.extend(spaces) if span.end >= end_offset else span586                for span in spans587            ]588            self._text.append(new_spaces)589            self._length += spaces590        else:591            self.plain += new_spaces592 593    def highlight_regex(594        self,595        re_highlight: Union[Pattern[str], str],596        style: Optional[Union[GetStyleCallable, StyleType]] = None,597        *,598        style_prefix: str = "",599    ) -> int:600        """Highlight text with a regular expression, where group names are601        translated to styles.602 603        Args:604            re_highlight (Union[re.Pattern, str]): A regular expression object or string.605            style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable606                which accepts the matched text and returns a style. Defaults to None.607            style_prefix (str, optional): Optional prefix to add to style group names.608 609        Returns:610            int: Number of regex matches611        """612        count = 0613        append_span = self._spans.append614        _Span = Span615        plain = self.plain616        if isinstance(re_highlight, str):617            re_highlight = re.compile(re_highlight)618        for match in re_highlight.finditer(plain):619            get_span = match.span620            if style:621                start, end = get_span()622                match_style = style(plain[start:end]) if callable(style) else style623                if match_style is not None and end > start:624                    append_span(_Span(start, end, match_style))625 626            count += 1627            for name in match.groupdict().keys():628                start, end = get_span(name)629                if start != -1 and end > start:630                    append_span(_Span(start, end, f"{style_prefix}{name}"))631        return count632 633    def highlight_words(634        self,635        words: Iterable[str],636        style: Union[str, Style],637        *,638        case_sensitive: bool = True,639    ) -> int:640        """Highlight words with a style.641 642        Args:643            words (Iterable[str]): Words to highlight.644            style (Union[str, Style]): Style to apply.645            case_sensitive (bool, optional): Enable case sensitive matching. Defaults to True.646 647        Returns:648            int: Number of words highlighted.649        """650        re_words = "|".join(re.escape(word) for word in words)651        add_span = self._spans.append652        count = 0653        _Span = Span654        for match in re.finditer(655            re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE656        ):657            start, end = match.span(0)658            add_span(_Span(start, end, style))659            count += 1660        return count661 662    def rstrip(self) -> None:663        """Strip whitespace from end of text."""664        self.plain = self.plain.rstrip()665 666    def rstrip_end(self, size: int) -> None:667        """Remove whitespace beyond a certain width at the end of the text.668 669        Args:670            size (int): The desired size of the text.671        """672        text_length = len(self)673        if text_length > size:674            excess = text_length - size675            whitespace_match = _re_whitespace.search(self.plain)676            if whitespace_match is not None:677                whitespace_count = len(whitespace_match.group(0))678                self.right_crop(min(whitespace_count, excess))679 680    def set_length(self, new_length: int) -> None:681        """Set new length of the text, clipping or padding is required."""682        length = len(self)683        if length != new_length:684            if length < new_length:685                self.pad_right(new_length - length)686            else:687                self.right_crop(length - new_length)688 689    def __rich_console__(690        self, console: "Console", options: "ConsoleOptions"691    ) -> Iterable[Segment]:692        tab_size: int = console.tab_size if self.tab_size is None else self.tab_size693        justify = self.justify or options.justify or DEFAULT_JUSTIFY694        overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW695 696        lines = self.wrap(697            console,698            options.max_width,699            justify=justify,700            overflow=overflow,701            tab_size=tab_size or 8,702            no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),703        )704        all_lines = Text("\n").join(lines)705        yield from all_lines.render(console, end=self.end)706 707    def __rich_measure__(708        self, console: "Console", options: "ConsoleOptions"709    ) -> Measurement:710        text = self.plain711        lines = text.splitlines()712        max_text_width = max(cell_len(line) for line in lines) if lines else 0713        words = text.split()714        min_text_width = (715            max(cell_len(word) for word in words) if words else max_text_width716        )717        return Measurement(min_text_width, max_text_width)718 719    def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:720        """Render the text as Segments.721 722        Args:723            console (Console): Console instance.724            end (Optional[str], optional): Optional end character.725 726        Returns:727            Iterable[Segment]: Result of render that may be written to the console.728        """729        _Segment = Segment730        text = self.plain731        if not self._spans:732            yield Segment(text)733            if end:734                yield _Segment(end)735            return736        get_style = partial(console.get_style, default=Style.null())737 738        enumerated_spans = list(enumerate(self._spans, 1))739        style_map = {index: get_style(span.style) for index, span in enumerated_spans}740        style_map[0] = get_style(self.style)741 742        spans = [743            (0, False, 0),744            *((span.start, False, index) for index, span in enumerated_spans),745            *((span.end, True, index) for index, span in enumerated_spans),746            (len(text), True, 0),747        ]748        spans.sort(key=itemgetter(0, 1))749 750        stack: List[int] = []751        stack_append = stack.append752        stack_pop = stack.remove753 754        style_cache: Dict[Tuple[Style, ...], Style] = {}755        style_cache_get = style_cache.get756        combine = Style.combine757 758        def get_current_style() -> Style:759            """Construct current style from stack."""760            styles = tuple(style_map[_style_id] for _style_id in sorted(stack))761            cached_style = style_cache_get(styles)762            if cached_style is not None:763                return cached_style764            current_style = combine(styles)765            style_cache[styles] = current_style766            return current_style767 768        for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):769            if leaving:770                stack_pop(style_id)771            else:772                stack_append(style_id)773            if next_offset > offset:774                yield _Segment(text[offset:next_offset], get_current_style())775        if end:776            yield _Segment(end)777 778    def join(self, lines: Iterable["Text"]) -> "Text":779        """Join text together with this instance as the separator.780 781        Args:782            lines (Iterable[Text]): An iterable of Text instances to join.783 784        Returns:785            Text: A new text instance containing join text.786        """787 788        new_text = self.blank_copy()789 790        def iter_text() -> Iterable["Text"]:791            if self.plain:792                for last, line in loop_last(lines):793                    yield line794                    if not last:795                        yield self796            else:797                yield from lines798 799        extend_text = new_text._text.extend800        append_span = new_text._spans.append801        extend_spans = new_text._spans.extend802        offset = 0803        _Span = Span804 805        for text in iter_text():806            extend_text(text._text)807            if text.style:808                append_span(_Span(offset, offset + len(text), text.style))809            extend_spans(810                _Span(offset + start, offset + end, style)811                for start, end, style in text._spans812            )813            offset += len(text)814        new_text._length = offset815        return new_text816 817    def expand_tabs(self, tab_size: Optional[int] = None) -> None:818        """Converts tabs to spaces.819 820        Args:821            tab_size (int, optional): Size of tabs. Defaults to 8.822 823        """824        if "\t" not in self.plain:825            return826        if tab_size is None:827            tab_size = self.tab_size828        if tab_size is None:829            tab_size = 8830 831        new_text: List[Text] = []832        append = new_text.append833 834        for line in self.split("\n", include_separator=True):835            if "\t" not in line.plain:836                append(line)837            else:838                cell_position = 0839                parts = line.split("\t", include_separator=True)840                for part in parts:841                    if part.plain.endswith("\t"):842                        part._text[-1] = part._text[-1][:-1] + " "843                        cell_position += part.cell_len844                        tab_remainder = cell_position % tab_size845                        if tab_remainder:846                            spaces = tab_size - tab_remainder847                            part.extend_style(spaces)848                            cell_position += spaces849                    else:850                        cell_position += part.cell_len851                    append(part)852 853        result = Text("").join(new_text)854 855        self._text = [result.plain]856        self._length = len(self.plain)857        self._spans[:] = result._spans858 859    def truncate(860        self,861        max_width: int,862        *,863        overflow: Optional["OverflowMethod"] = None,864        pad: bool = False,865    ) -> None:866        """Truncate text if it is longer that a given width.867 868        Args:869            max_width (int): Maximum number of characters in text.870            overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.871            pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.872        """873        _overflow = overflow or self.overflow or DEFAULT_OVERFLOW874        if _overflow != "ignore":875            length = cell_len(self.plain)876            if length > max_width:877                if _overflow == "ellipsis":878                    self.plain = set_cell_size(self.plain, max_width - 1) + "…"879                else:880                    self.plain = set_cell_size(self.plain, max_width)881            if pad and length < max_width:882                spaces = max_width - length883                self._text = [f"{self.plain}{' ' * spaces}"]884                self._length = len(self.plain)885 886    def _trim_spans(self) -> None:887        """Remove or modify any spans that are over the end of the text."""888        max_offset = len(self.plain)889        _Span = Span890        self._spans[:] = [891            (892                span893                if span.end < max_offset894                else _Span(span.start, min(max_offset, span.end), span.style)895            )896            for span in self._spans897            if span.start < max_offset898        ]899 900    def pad(self, count: int, character: str = " ") -> None:901        """Pad left and right with a given number of characters.902 903        Args:904            count (int): Width of padding.905            character (str): The character to pad with. Must be a string of length 1.906        """907        assert len(character) == 1, "Character must be a string of length 1"908        if count:909            pad_characters = character * count910            self.plain = f"{pad_characters}{self.plain}{pad_characters}"911            _Span = Span912            self._spans[:] = [913                _Span(start + count, end + count, style)914                for start, end, style in self._spans915            ]916 917    def pad_left(self, count: int, character: str = " ") -> None:918        """Pad the left with a given character.919 920        Args:921            count (int): Number of characters to pad.922            character (str, optional): Character to pad with. Defaults to " ".923        """924        assert len(character) == 1, "Character must be a string of length 1"925        if count:926            self.plain = f"{character * count}{self.plain}"927            _Span = Span928            self._spans[:] = [929                _Span(start + count, end + count, style)930                for start, end, style in self._spans931            ]932 933    def pad_right(self, count: int, character: str = " ") -> None:934        """Pad the right with a given character.935 936        Args:937            count (int): Number of characters to pad.938            character (str, optional): Character to pad with. Defaults to " ".939        """940        assert len(character) == 1, "Character must be a string of length 1"941        if count:942            self.plain = f"{self.plain}{character * count}"943 944    def align(self, align: AlignMethod, width: int, character: str = " ") -> None:945        """Align text to a given width.946 947        Args:948            align (AlignMethod): One of "left", "center", or "right".949            width (int): Desired width.950            character (str, optional): Character to pad with. Defaults to " ".951        """952        self.truncate(width)953        excess_space = width - cell_len(self.plain)954        if excess_space:955            if align == "left":956                self.pad_right(excess_space, character)957            elif align == "center":958                left = excess_space // 2959                self.pad_left(left, character)960                self.pad_right(excess_space - left, character)961            else:962                self.pad_left(excess_space, character)963 964    def append(965        self, text: Union["Text", str], style: Optional[Union[str, "Style"]] = None966    ) -> "Text":967        """Add text with an optional style.968 969        Args:970            text (Union[Text, str]): A str or Text to append.971            style (str, optional): A style name. Defaults to None.972 973        Returns:974            Text: Returns self for chaining.975        """976 977        if not isinstance(text, (str, Text)):978            raise TypeError("Only str or Text can be appended to Text")979 980        if len(text):981            if isinstance(text, str):982                sanitized_text = strip_control_codes(text)983                self._text.append(sanitized_text)984                offset = len(self)985                text_length = len(sanitized_text)986                if style:987                    self._spans.append(Span(offset, offset + text_length, style))988                self._length += text_length989            elif isinstance(text, Text):990                _Span = Span991                if style is not None:992                    raise ValueError(993                        "style must not be set when appending Text instance"994                    )995                text_length = self._length996                if text.style:997                    self._spans.append(998                        _Span(text_length, text_length + len(text), text.style)999                    )1000                self._text.append(text.plain)1001                self._spans.extend(1002                    _Span(start + text_length, end + text_length, style)1003                    for start, end, style in text._spans.copy()1004                )1005                self._length += len(text)1006        return self1007 1008    def append_text(self, text: "Text") -> "Text":1009        """Append another Text instance. This method is more performant than Text.append, but1010        only works for Text.1011 1012        Args:1013            text (Text): The Text instance to append to this instance.1014 1015        Returns:1016            Text: Returns self for chaining.1017        """1018        _Span = Span1019        text_length = self._length1020        if text.style:1021            self._spans.append(_Span(text_length, text_length + len(text), text.style))1022        self._text.append(text.plain)1023        self._spans.extend(1024            _Span(start + text_length, end + text_length, style)1025            for start, end, style in text._spans.copy()1026        )1027        self._length += len(text)1028        return self1029 1030    def append_tokens(1031        self, tokens: Iterable[Tuple[str, Optional[StyleType]]]1032    ) -> "Text":1033        """Append iterable of str and style. Style may be a Style instance or a str style definition.1034 1035        Args:1036            tokens (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.1037 1038        Returns:1039            Text: Returns self for chaining.1040        """1041        append_text = self._text.append1042        append_span = self._spans.append1043        _Span = Span1044        offset = len(self)1045        for content, style in tokens:1046            content = strip_control_codes(content)1047            append_text(content)1048            if style:1049                append_span(_Span(offset, offset + len(content), style))1050            offset += len(content)1051        self._length = offset1052        return self1053 1054    def copy_styles(self, text: "Text") -> None:1055        """Copy styles from another Text instance.1056 1057        Args:1058            text (Text): A Text instance to copy styles from, must be the same length.1059        """1060        self._spans.extend(text._spans)1061 1062    def split(1063        self,1064        separator: str = "\n",1065        *,1066        include_separator: bool = False,1067        allow_blank: bool = False,1068    ) -> Lines:1069        """Split rich text in to lines, preserving styles.1070 1071        Args:1072            separator (str, optional): String to split on. Defaults to "\\\\n".1073            include_separator (bool, optional): Include the separator in the lines. Defaults to False.1074            allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.1075 1076        Returns:1077            List[RichText]: A list of rich text, one per line of the original.1078        """1079        assert separator, "separator must not be empty"1080 1081        text = self.plain1082        if separator not in text:1083            return Lines([self.copy()])1084 1085        if include_separator:1086            lines = self.divide(1087                match.end() for match in re.finditer(re.escape(separator), text)1088            )1089        else:1090 1091            def flatten_spans() -> Iterable[int]:1092                for match in re.finditer(re.escape(separator), text):1093                    start, end = match.span()1094                    yield start1095                    yield end1096 1097            lines = Lines(1098                line for line in self.divide(flatten_spans()) if line.plain != separator1099            )1100 1101        if not allow_blank and text.endswith(separator):1102            lines.pop()1103 1104        return lines1105 1106    def divide(self, offsets: Iterable[int]) -> Lines:1107        """Divide text into a number of lines at given offsets.1108 1109        Args:1110            offsets (Iterable[int]): Offsets used to divide text.1111 1112        Returns:1113            Lines: New RichText instances between offsets.1114        """1115        _offsets = list(offsets)1116 1117        if not _offsets:1118            return Lines([self.copy()])1119 1120        text = self.plain1121        text_length = len(text)1122        divide_offsets = [0, *_offsets, text_length]1123        line_ranges = list(zip(divide_offsets, divide_offsets[1:]))1124 1125        style = self.style1126        justify = self.justify1127        overflow = self.overflow1128        _Text = Text1129        new_lines = Lines(1130            _Text(1131                text[start:end],1132                style=style,1133                justify=justify,1134                overflow=overflow,1135            )1136            for start, end in line_ranges1137        )1138        if not self._spans:1139            return new_lines1140 1141        _line_appends = [line._spans.append for line in new_lines._lines]1142        line_count = len(line_ranges)1143        _Span = Span1144 1145        for span_start, span_end, style in self._spans:1146            lower_bound = 01147            upper_bound = line_count1148            start_line_no = (lower_bound + upper_bound) // 21149 1150            while True:1151                line_start, line_end = line_ranges[start_line_no]1152                if span_start < line_start:1153                    upper_bound = start_line_no - 11154                elif span_start > line_end:1155                    lower_bound = start_line_no + 11156                else:1157                    break1158                start_line_no = (lower_bound + upper_bound) // 21159 1160            if span_end < line_end:1161                end_line_no = start_line_no1162            else:1163                end_line_no = lower_bound = start_line_no1164                upper_bound = line_count1165 1166                while True:1167                    line_start, line_end = line_ranges[end_line_no]1168                    if span_end < line_start:1169                        upper_bound = end_line_no - 11170                    elif span_end > line_end:1171                        lower_bound = end_line_no + 11172                    else:1173                        break1174                    end_line_no = (lower_bound + upper_bound) // 21175 1176            for line_no in range(start_line_no, end_line_no + 1):1177                line_start, line_end = line_ranges[line_no]1178                new_start = max(0, span_start - line_start)1179                new_end = min(span_end - line_start, line_end - line_start)1180                if new_end > new_start:1181                    _line_appends[line_no](_Span(new_start, new_end, style))1182 1183        return new_lines1184 1185    def right_crop(self, amount: int = 1) -> None:1186        """Remove a number of characters from the end of the text."""1187        max_offset = len(self.plain) - amount1188        _Span = Span1189        self._spans[:] = [1190            (1191                span1192                if span.end < max_offset1193                else _Span(span.start, min(max_offset, span.end), span.style)1194            )1195            for span in self._spans1196            if span.start < max_offset1197        ]1198        self._text = [self.plain[:-amount]]1199        self._length -= amount1200 

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

codekingpro/portable-devtools · Team Ai