python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
rich/
text.py
       1  import re
       2  from functools import partial, reduce
       3  from math import gcd
       4  from operator import itemgetter
       5  from 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  
      18  from ._loop import loop_last
      19  from ._pick import pick_bool
      20  from ._wrap import divide_line
      21  from .align import AlignMethod
      22  from .cells import cell_len, set_cell_size
      23  from .containers import Lines
      24  from .control import strip_control_codes
      25  from .emoji import EmojiVariant
      26  from .jupyter import JupyterMixin
      27  from .measure import Measurement
      28  from .segment import Segment
      29  from .style import Style, StyleType
      30  
      31  if TYPE_CHECKING:  # pragma: no cover
      32      from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod
      33  
      34  DEFAULT_JUSTIFY: "JustifyMethod" = "default"
      35  DEFAULT_OVERFLOW: "OverflowMethod" = "fold"
      36  
      37  
      38  _re_whitespace = re.compile(r"\s+$")
      39  
      40  TextType = Union[str, "Text"]
      41  
      42  GetStyleCallable = Callable[[str], Optional[StyleType]]
      43  
      44  
      45  class ESC[4;38;5;81mSpan(ESC[4;38;5;149mNamedTuple):
      46      """A marked up region in some text."""
      47  
      48      start: int
      49      """Span start index."""
      50      end: int
      51      """Span end index."""
      52      style: Union[str, Style]
      53      """Style associated with the span."""
      54  
      55      def __repr__(self) -> str:
      56          return f"Span({self.start}, {self.end}, {self.style!r})"
      57  
      58      def __bool__(self) -> bool:
      59          return self.end > self.start
      60  
      61      def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:
      62          """Split a span in to 2 from a given offset."""
      63  
      64          if offset < self.start:
      65              return self, None
      66          if offset >= self.end:
      67              return self, None
      68  
      69          start, end, style = self
      70          span1 = Span(start, min(end, offset), style)
      71          span2 = Span(span1.end, end, style)
      72          return span1, span2
      73  
      74      def move(self, offset: int) -> "Span":
      75          """Move start and end by a given offset.
      76  
      77          Args:
      78              offset (int): Number of characters to add to start and end.
      79  
      80          Returns:
      81              TextSpan: A new TextSpan with adjusted position.
      82          """
      83          start, end, style = self
      84          return Span(start + offset, end + offset, style)
      85  
      86      def right_crop(self, offset: int) -> "Span":
      87          """Crop the span at the given offset.
      88  
      89          Args:
      90              offset (int): A value between start and end.
      91  
      92          Returns:
      93              Span: A new (possibly smaller) span.
      94          """
      95          start, end, style = self
      96          if offset >= end:
      97              return self
      98          return Span(start, min(offset, end), style)
      99  
     100  
     101  class ESC[4;38;5;81mText(ESC[4;38;5;149mJupyterMixin):
     102      """Text with color / style.
     103  
     104      Args:
     105          text (str, optional): Default unstyled text. Defaults to "".
     106          style (Union[str, Style], optional): Base style for text. Defaults to "".
     107          justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
     108          overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
     109          no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
     110          end (str, optional): Character to end text with. Defaults to "\\\\n".
     111          tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
     112          spans (List[Span], optional). A list of predefined style spans. Defaults to None.
     113      """
     114  
     115      __slots__ = [
     116          "_text",
     117          "style",
     118          "justify",
     119          "overflow",
     120          "no_wrap",
     121          "end",
     122          "tab_size",
     123          "_spans",
     124          "_length",
     125      ]
     126  
     127      def __init__(
     128          self,
     129          text: str = "",
     130          style: Union[str, Style] = "",
     131          *,
     132          justify: Optional["JustifyMethod"] = None,
     133          overflow: Optional["OverflowMethod"] = None,
     134          no_wrap: Optional[bool] = None,
     135          end: str = "\n",
     136          tab_size: Optional[int] = 8,
     137          spans: Optional[List[Span]] = None,
     138      ) -> None:
     139          sanitized_text = strip_control_codes(text)
     140          self._text = [sanitized_text]
     141          self.style = style
     142          self.justify: Optional["JustifyMethod"] = justify
     143          self.overflow: Optional["OverflowMethod"] = overflow
     144          self.no_wrap = no_wrap
     145          self.end = end
     146          self.tab_size = tab_size
     147          self._spans: List[Span] = spans or []
     148          self._length: int = len(sanitized_text)
     149  
     150      def __len__(self) -> int:
     151          return self._length
     152  
     153      def __bool__(self) -> bool:
     154          return bool(self._length)
     155  
     156      def __str__(self) -> str:
     157          return self.plain
     158  
     159      def __repr__(self) -> str:
     160          return f"<text {self.plain!r} {self._spans!r}>"
     161  
     162      def __add__(self, other: Any) -> "Text":
     163          if isinstance(other, (str, Text)):
     164              result = self.copy()
     165              result.append(other)
     166              return result
     167          return NotImplemented
     168  
     169      def __eq__(self, other: object) -> bool:
     170          if not isinstance(other, Text):
     171              return NotImplemented
     172          return self.plain == other.plain and self._spans == other._spans
     173  
     174      def __contains__(self, other: object) -> bool:
     175          if isinstance(other, str):
     176              return other in self.plain
     177          elif isinstance(other, Text):
     178              return other.plain in self.plain
     179          return False
     180  
     181      def __getitem__(self, slice: Union[int, slice]) -> "Text":
     182          def get_text_at(offset: int) -> "Text":
     183              _Span = Span
     184              text = Text(
     185                  self.plain[offset],
     186                  spans=[
     187                      _Span(0, 1, style)
     188                      for start, end, style in self._spans
     189                      if end > offset >= start
     190                  ],
     191                  end="",
     192              )
     193              return text
     194  
     195          if isinstance(slice, int):
     196              return get_text_at(slice)
     197          else:
     198              start, stop, step = slice.indices(len(self.plain))
     199              if step == 1:
     200                  lines = self.divide([start, stop])
     201                  return lines[1]
     202              else:
     203                  # This would be a bit of work to implement efficiently
     204                  # For now, its not required
     205                  raise TypeError("slices with step!=1 are not supported")
     206  
     207      @property
     208      def cell_len(self) -> int:
     209          """Get the number of cells required to render this text."""
     210          return cell_len(self.plain)
     211  
     212      @property
     213      def markup(self) -> str:
     214          """Get console markup to render this Text.
     215  
     216          Returns:
     217              str: A string potentially creating markup tags.
     218          """
     219          from .markup import escape
     220  
     221          output: List[str] = []
     222  
     223          plain = self.plain
     224          markup_spans = [
     225              (0, False, self.style),
     226              *((span.start, False, span.style) for span in self._spans),
     227              *((span.end, True, span.style) for span in self._spans),
     228              (len(plain), True, self.style),
     229          ]
     230          markup_spans.sort(key=itemgetter(0, 1))
     231          position = 0
     232          append = output.append
     233          for offset, closing, style in markup_spans:
     234              if offset > position:
     235                  append(escape(plain[position:offset]))
     236                  position = offset
     237              if style:
     238                  append(f"[/{style}]" if closing else f"[{style}]")
     239          markup = "".join(output)
     240          return markup
     241  
     242      @classmethod
     243      def from_markup(
     244          cls,
     245          text: str,
     246          *,
     247          style: Union[str, Style] = "",
     248          emoji: bool = True,
     249          emoji_variant: Optional[EmojiVariant] = None,
     250          justify: Optional["JustifyMethod"] = None,
     251          overflow: Optional["OverflowMethod"] = None,
     252          end: str = "\n",
     253      ) -> "Text":
     254          """Create Text instance from markup.
     255  
     256          Args:
     257              text (str): A string containing console markup.
     258              emoji (bool, optional): Also render emoji code. Defaults to True.
     259              justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
     260              overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
     261              end (str, optional): Character to end text with. Defaults to "\\\\n".
     262  
     263          Returns:
     264              Text: A Text instance with markup rendered.
     265          """
     266          from .markup import render
     267  
     268          rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant)
     269          rendered_text.justify = justify
     270          rendered_text.overflow = overflow
     271          rendered_text.end = end
     272          return rendered_text
     273  
     274      @classmethod
     275      def from_ansi(
     276          cls,
     277          text: str,
     278          *,
     279          style: Union[str, Style] = "",
     280          justify: Optional["JustifyMethod"] = None,
     281          overflow: Optional["OverflowMethod"] = None,
     282          no_wrap: Optional[bool] = None,
     283          end: str = "\n",
     284          tab_size: Optional[int] = 8,
     285      ) -> "Text":
     286          """Create a Text object from a string containing ANSI escape codes.
     287  
     288          Args:
     289              text (str): A string containing escape codes.
     290              style (Union[str, Style], optional): Base style for text. Defaults to "".
     291              justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
     292              overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
     293              no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
     294              end (str, optional): Character to end text with. Defaults to "\\\\n".
     295              tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
     296          """
     297          from .ansi import AnsiDecoder
     298  
     299          joiner = Text(
     300              "\n",
     301              justify=justify,
     302              overflow=overflow,
     303              no_wrap=no_wrap,
     304              end=end,
     305              tab_size=tab_size,
     306              style=style,
     307          )
     308          decoder = AnsiDecoder()
     309          result = joiner.join(line for line in decoder.decode(text))
     310          return result
     311  
     312      @classmethod
     313      def styled(
     314          cls,
     315          text: str,
     316          style: StyleType = "",
     317          *,
     318          justify: Optional["JustifyMethod"] = None,
     319          overflow: Optional["OverflowMethod"] = None,
     320      ) -> "Text":
     321          """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used
     322          to pad the text when it is justified.
     323  
     324          Args:
     325              text (str): A string containing console markup.
     326              style (Union[str, Style]): Style to apply to the text. Defaults to "".
     327              justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
     328              overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
     329  
     330          Returns:
     331              Text: A text instance with a style applied to the entire string.
     332          """
     333          styled_text = cls(text, justify=justify, overflow=overflow)
     334          styled_text.stylize(style)
     335          return styled_text
     336  
     337      @classmethod
     338      def assemble(
     339          cls,
     340          *parts: Union[str, "Text", Tuple[str, StyleType]],
     341          style: Union[str, Style] = "",
     342          justify: Optional["JustifyMethod"] = None,
     343          overflow: Optional["OverflowMethod"] = None,
     344          no_wrap: Optional[bool] = None,
     345          end: str = "\n",
     346          tab_size: int = 8,
     347          meta: Optional[Dict[str, Any]] = None,
     348      ) -> "Text":
     349          """Construct a text instance by combining a sequence of strings with optional styles.
     350          The positional arguments should be either strings, or a tuple of string + style.
     351  
     352          Args:
     353              style (Union[str, Style], optional): Base style for text. Defaults to "".
     354              justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
     355              overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
     356              end (str, optional): Character to end text with. Defaults to "\\\\n".
     357              tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
     358              meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None
     359  
     360          Returns:
     361              Text: A new text instance.
     362          """
     363          text = cls(
     364              style=style,
     365              justify=justify,
     366              overflow=overflow,
     367              no_wrap=no_wrap,
     368              end=end,
     369              tab_size=tab_size,
     370          )
     371          append = text.append
     372          _Text = Text
     373          for part in parts:
     374              if isinstance(part, (_Text, str)):
     375                  append(part)
     376              else:
     377                  append(*part)
     378          if meta:
     379              text.apply_meta(meta)
     380          return text
     381  
     382      @property
     383      def plain(self) -> str:
     384          """Get the text as a single string."""
     385          if len(self._text) != 1:
     386              self._text[:] = ["".join(self._text)]
     387          return self._text[0]
     388  
     389      @plain.setter
     390      def plain(self, new_text: str) -> None:
     391          """Set the text to a new value."""
     392          if new_text != self.plain:
     393              sanitized_text = strip_control_codes(new_text)
     394              self._text[:] = [sanitized_text]
     395              old_length = self._length
     396              self._length = len(sanitized_text)
     397              if old_length > self._length:
     398                  self._trim_spans()
     399  
     400      @property
     401      def spans(self) -> List[Span]:
     402          """Get a reference to the internal list of spans."""
     403          return self._spans
     404  
     405      @spans.setter
     406      def spans(self, spans: List[Span]) -> None:
     407          """Set spans."""
     408          self._spans = spans[:]
     409  
     410      def blank_copy(self, plain: str = "") -> "Text":
     411          """Return a new Text instance with copied meta data (but not the string or spans)."""
     412          copy_self = Text(
     413              plain,
     414              style=self.style,
     415              justify=self.justify,
     416              overflow=self.overflow,
     417              no_wrap=self.no_wrap,
     418              end=self.end,
     419              tab_size=self.tab_size,
     420          )
     421          return copy_self
     422  
     423      def copy(self) -> "Text":
     424          """Return a copy of this instance."""
     425          copy_self = Text(
     426              self.plain,
     427              style=self.style,
     428              justify=self.justify,
     429              overflow=self.overflow,
     430              no_wrap=self.no_wrap,
     431              end=self.end,
     432              tab_size=self.tab_size,
     433          )
     434          copy_self._spans[:] = self._spans
     435          return copy_self
     436  
     437      def stylize(
     438          self,
     439          style: Union[str, Style],
     440          start: int = 0,
     441          end: Optional[int] = None,
     442      ) -> None:
     443          """Apply a style to the text, or a portion of the text.
     444  
     445          Args:
     446              style (Union[str, Style]): Style instance or style definition to apply.
     447              start (int): Start offset (negative indexing is supported). Defaults to 0.
     448              end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
     449          """
     450          if style:
     451              length = len(self)
     452              if start < 0:
     453                  start = length + start
     454              if end is None:
     455                  end = length
     456              if end < 0:
     457                  end = length + end
     458              if start >= length or end <= start:
     459                  # Span not in text or not valid
     460                  return
     461              self._spans.append(Span(start, min(length, end), style))
     462  
     463      def stylize_before(
     464          self,
     465          style: Union[str, Style],
     466          start: int = 0,
     467          end: Optional[int] = None,
     468      ) -> None:
     469          """Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.
     470  
     471          Args:
     472              style (Union[str, Style]): Style instance or style definition to apply.
     473              start (int): Start offset (negative indexing is supported). Defaults to 0.
     474              end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
     475          """
     476          if style:
     477              length = len(self)
     478              if start < 0:
     479                  start = length + start
     480              if end is None:
     481                  end = length
     482              if end < 0:
     483                  end = length + end
     484              if start >= length or end <= start:
     485                  # Span not in text or not valid
     486                  return
     487              self._spans.insert(0, Span(start, min(length, end), style))
     488  
     489      def apply_meta(
     490          self, meta: Dict[str, Any], start: int = 0, end: Optional[int] = None
     491      ) -> None:
     492          """Apply meta data to the text, or a portion of the text.
     493  
     494          Args:
     495              meta (Dict[str, Any]): A dict of meta information.
     496              start (int): Start offset (negative indexing is supported). Defaults to 0.
     497              end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
     498  
     499          """
     500          style = Style.from_meta(meta)
     501          self.stylize(style, start=start, end=end)
     502  
     503      def on(self, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Text":
     504          """Apply event handlers (used by Textual project).
     505  
     506          Example:
     507              >>> from rich.text import Text
     508              >>> text = Text("hello world")
     509              >>> text.on(click="view.toggle('world')")
     510  
     511          Args:
     512              meta (Dict[str, Any]): Mapping of meta information.
     513              **handlers: Keyword args are prefixed with "@" to defined handlers.
     514  
     515          Returns:
     516              Text: Self is returned to method may be chained.
     517          """
     518          meta = {} if meta is None else meta
     519          meta.update({f"@{key}": value for key, value in handlers.items()})
     520          self.stylize(Style.from_meta(meta))
     521          return self
     522  
     523      def remove_suffix(self, suffix: str) -> None:
     524          """Remove a suffix if it exists.
     525  
     526          Args:
     527              suffix (str): Suffix to remove.
     528          """
     529          if self.plain.endswith(suffix):
     530              self.right_crop(len(suffix))
     531  
     532      def get_style_at_offset(self, console: "Console", offset: int) -> Style:
     533          """Get the style of a character at give offset.
     534  
     535          Args:
     536              console (~Console): Console where text will be rendered.
     537              offset (int): Offset in to text (negative indexing supported)
     538  
     539          Returns:
     540              Style: A Style instance.
     541          """
     542          # TODO: This is a little inefficient, it is only used by full justify
     543          if offset < 0:
     544              offset = len(self) + offset
     545          get_style = console.get_style
     546          style = get_style(self.style).copy()
     547          for start, end, span_style in self._spans:
     548              if end > offset >= start:
     549                  style += get_style(span_style, default="")
     550          return style
     551  
     552      def highlight_regex(
     553          self,
     554          re_highlight: str,
     555          style: Optional[Union[GetStyleCallable, StyleType]] = None,
     556          *,
     557          style_prefix: str = "",
     558      ) -> int:
     559          """Highlight text with a regular expression, where group names are
     560          translated to styles.
     561  
     562          Args:
     563              re_highlight (str): A regular expression.
     564              style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable
     565                  which accepts the matched text and returns a style. Defaults to None.
     566              style_prefix (str, optional): Optional prefix to add to style group names.
     567  
     568          Returns:
     569              int: Number of regex matches
     570          """
     571          count = 0
     572          append_span = self._spans.append
     573          _Span = Span
     574          plain = self.plain
     575          for match in re.finditer(re_highlight, plain):
     576              get_span = match.span
     577              if style:
     578                  start, end = get_span()
     579                  match_style = style(plain[start:end]) if callable(style) else style
     580                  if match_style is not None and end > start:
     581                      append_span(_Span(start, end, match_style))
     582  
     583              count += 1
     584              for name in match.groupdict().keys():
     585                  start, end = get_span(name)
     586                  if start != -1 and end > start:
     587                      append_span(_Span(start, end, f"{style_prefix}{name}"))
     588          return count
     589  
     590      def highlight_words(
     591          self,
     592          words: Iterable[str],
     593          style: Union[str, Style],
     594          *,
     595          case_sensitive: bool = True,
     596      ) -> int:
     597          """Highlight words with a style.
     598  
     599          Args:
     600              words (Iterable[str]): Worlds to highlight.
     601              style (Union[str, Style]): Style to apply.
     602              case_sensitive (bool, optional): Enable case sensitive matchings. Defaults to True.
     603  
     604          Returns:
     605              int: Number of words highlighted.
     606          """
     607          re_words = "|".join(re.escape(word) for word in words)
     608          add_span = self._spans.append
     609          count = 0
     610          _Span = Span
     611          for match in re.finditer(
     612              re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE
     613          ):
     614              start, end = match.span(0)
     615              add_span(_Span(start, end, style))
     616              count += 1
     617          return count
     618  
     619      def rstrip(self) -> None:
     620          """Strip whitespace from end of text."""
     621          self.plain = self.plain.rstrip()
     622  
     623      def rstrip_end(self, size: int) -> None:
     624          """Remove whitespace beyond a certain width at the end of the text.
     625  
     626          Args:
     627              size (int): The desired size of the text.
     628          """
     629          text_length = len(self)
     630          if text_length > size:
     631              excess = text_length - size
     632              whitespace_match = _re_whitespace.search(self.plain)
     633              if whitespace_match is not None:
     634                  whitespace_count = len(whitespace_match.group(0))
     635                  self.right_crop(min(whitespace_count, excess))
     636  
     637      def set_length(self, new_length: int) -> None:
     638          """Set new length of the text, clipping or padding is required."""
     639          length = len(self)
     640          if length != new_length:
     641              if length < new_length:
     642                  self.pad_right(new_length - length)
     643              else:
     644                  self.right_crop(length - new_length)
     645  
     646      def __rich_console__(
     647          self, console: "Console", options: "ConsoleOptions"
     648      ) -> Iterable[Segment]:
     649          tab_size: int = console.tab_size or self.tab_size or 8
     650          justify = self.justify or options.justify or DEFAULT_JUSTIFY
     651  
     652          overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW
     653  
     654          lines = self.wrap(
     655              console,
     656              options.max_width,
     657              justify=justify,
     658              overflow=overflow,
     659              tab_size=tab_size or 8,
     660              no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),
     661          )
     662          all_lines = Text("\n").join(lines)
     663          yield from all_lines.render(console, end=self.end)
     664  
     665      def __rich_measure__(
     666          self, console: "Console", options: "ConsoleOptions"
     667      ) -> Measurement:
     668          text = self.plain
     669          lines = text.splitlines()
     670          max_text_width = max(cell_len(line) for line in lines) if lines else 0
     671          words = text.split()
     672          min_text_width = (
     673              max(cell_len(word) for word in words) if words else max_text_width
     674          )
     675          return Measurement(min_text_width, max_text_width)
     676  
     677      def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
     678          """Render the text as Segments.
     679  
     680          Args:
     681              console (Console): Console instance.
     682              end (Optional[str], optional): Optional end character.
     683  
     684          Returns:
     685              Iterable[Segment]: Result of render that may be written to the console.
     686          """
     687          _Segment = Segment
     688          text = self.plain
     689          if not self._spans:
     690              yield Segment(text)
     691              if end:
     692                  yield _Segment(end)
     693              return
     694          get_style = partial(console.get_style, default=Style.null())
     695  
     696          enumerated_spans = list(enumerate(self._spans, 1))
     697          style_map = {index: get_style(span.style) for index, span in enumerated_spans}
     698          style_map[0] = get_style(self.style)
     699  
     700          spans = [
     701              (0, False, 0),
     702              *((span.start, False, index) for index, span in enumerated_spans),
     703              *((span.end, True, index) for index, span in enumerated_spans),
     704              (len(text), True, 0),
     705          ]
     706          spans.sort(key=itemgetter(0, 1))
     707  
     708          stack: List[int] = []
     709          stack_append = stack.append
     710          stack_pop = stack.remove
     711  
     712          style_cache: Dict[Tuple[Style, ...], Style] = {}
     713          style_cache_get = style_cache.get
     714          combine = Style.combine
     715  
     716          def get_current_style() -> Style:
     717              """Construct current style from stack."""
     718              styles = tuple(style_map[_style_id] for _style_id in sorted(stack))
     719              cached_style = style_cache_get(styles)
     720              if cached_style is not None:
     721                  return cached_style
     722              current_style = combine(styles)
     723              style_cache[styles] = current_style
     724              return current_style
     725  
     726          for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
     727              if leaving:
     728                  stack_pop(style_id)
     729              else:
     730                  stack_append(style_id)
     731              if next_offset > offset:
     732                  yield _Segment(text[offset:next_offset], get_current_style())
     733          if end:
     734              yield _Segment(end)
     735  
     736      def join(self, lines: Iterable["Text"]) -> "Text":
     737          """Join text together with this instance as the separator.
     738  
     739          Args:
     740              lines (Iterable[Text]): An iterable of Text instances to join.
     741  
     742          Returns:
     743              Text: A new text instance containing join text.
     744          """
     745  
     746          new_text = self.blank_copy()
     747  
     748          def iter_text() -> Iterable["Text"]:
     749              if self.plain:
     750                  for last, line in loop_last(lines):
     751                      yield line
     752                      if not last:
     753                          yield self
     754              else:
     755                  yield from lines
     756  
     757          extend_text = new_text._text.extend
     758          append_span = new_text._spans.append
     759          extend_spans = new_text._spans.extend
     760          offset = 0
     761          _Span = Span
     762  
     763          for text in iter_text():
     764              extend_text(text._text)
     765              if text.style:
     766                  append_span(_Span(offset, offset + len(text), text.style))
     767              extend_spans(
     768                  _Span(offset + start, offset + end, style)
     769                  for start, end, style in text._spans
     770              )
     771              offset += len(text)
     772          new_text._length = offset
     773          return new_text
     774  
     775      def expand_tabs(self, tab_size: Optional[int] = None) -> None:
     776          """Converts tabs to spaces.
     777  
     778          Args:
     779              tab_size (int, optional): Size of tabs. Defaults to 8.
     780  
     781          """
     782          if "\t" not in self.plain:
     783              return
     784          pos = 0
     785          if tab_size is None:
     786              tab_size = self.tab_size
     787          assert tab_size is not None
     788          result = self.blank_copy()
     789          append = result.append
     790  
     791          _style = self.style
     792          for line in self.split("\n", include_separator=True):
     793              parts = line.split("\t", include_separator=True)
     794              for part in parts:
     795                  if part.plain.endswith("\t"):
     796                      part._text = [part.plain[:-1] + " "]
     797                      append(part)
     798                      pos += len(part)
     799                      spaces = tab_size - ((pos - 1) % tab_size) - 1
     800                      if spaces:
     801                          append(" " * spaces, _style)
     802                          pos += spaces
     803                  else:
     804                      append(part)
     805          self._text = [result.plain]
     806          self._length = len(self.plain)
     807          self._spans[:] = result._spans
     808  
     809      def truncate(
     810          self,
     811          max_width: int,
     812          *,
     813          overflow: Optional["OverflowMethod"] = None,
     814          pad: bool = False,
     815      ) -> None:
     816          """Truncate text if it is longer that a given width.
     817  
     818          Args:
     819              max_width (int): Maximum number of characters in text.
     820              overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.
     821              pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.
     822          """
     823          _overflow = overflow or self.overflow or DEFAULT_OVERFLOW
     824          if _overflow != "ignore":
     825              length = cell_len(self.plain)
     826              if length > max_width:
     827                  if _overflow == "ellipsis":
     828                      self.plain = set_cell_size(self.plain, max_width - 1) + ""
     829                  else:
     830                      self.plain = set_cell_size(self.plain, max_width)
     831              if pad and length < max_width:
     832                  spaces = max_width - length
     833                  self._text = [f"{self.plain}{' ' * spaces}"]
     834                  self._length = len(self.plain)
     835  
     836      def _trim_spans(self) -> None:
     837          """Remove or modify any spans that are over the end of the text."""
     838          max_offset = len(self.plain)
     839          _Span = Span
     840          self._spans[:] = [
     841              (
     842                  span
     843                  if span.end < max_offset
     844                  else _Span(span.start, min(max_offset, span.end), span.style)
     845              )
     846              for span in self._spans
     847              if span.start < max_offset
     848          ]
     849  
     850      def pad(self, count: int, character: str = " ") -> None:
     851          """Pad left and right with a given number of characters.
     852  
     853          Args:
     854              count (int): Width of padding.
     855          """
     856          assert len(character) == 1, "Character must be a string of length 1"
     857          if count:
     858              pad_characters = character * count
     859              self.plain = f"{pad_characters}{self.plain}{pad_characters}"
     860              _Span = Span
     861              self._spans[:] = [
     862                  _Span(start + count, end + count, style)
     863                  for start, end, style in self._spans
     864              ]
     865  
     866      def pad_left(self, count: int, character: str = " ") -> None:
     867          """Pad the left with a given character.
     868  
     869          Args:
     870              count (int): Number of characters to pad.
     871              character (str, optional): Character to pad with. Defaults to " ".
     872          """
     873          assert len(character) == 1, "Character must be a string of length 1"
     874          if count:
     875              self.plain = f"{character * count}{self.plain}"
     876              _Span = Span
     877              self._spans[:] = [
     878                  _Span(start + count, end + count, style)
     879                  for start, end, style in self._spans
     880              ]
     881  
     882      def pad_right(self, count: int, character: str = " ") -> None:
     883          """Pad the right with a given character.
     884  
     885          Args:
     886              count (int): Number of characters to pad.
     887              character (str, optional): Character to pad with. Defaults to " ".
     888          """
     889          assert len(character) == 1, "Character must be a string of length 1"
     890          if count:
     891              self.plain = f"{self.plain}{character * count}"
     892  
     893      def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
     894          """Align text to a given width.
     895  
     896          Args:
     897              align (AlignMethod): One of "left", "center", or "right".
     898              width (int): Desired width.
     899              character (str, optional): Character to pad with. Defaults to " ".
     900          """
     901          self.truncate(width)
     902          excess_space = width - cell_len(self.plain)
     903          if excess_space:
     904              if align == "left":
     905                  self.pad_right(excess_space, character)
     906              elif align == "center":
     907                  left = excess_space // 2
     908                  self.pad_left(left, character)
     909                  self.pad_right(excess_space - left, character)
     910              else:
     911                  self.pad_left(excess_space, character)
     912  
     913      def append(
     914          self, text: Union["Text", str], style: Optional[Union[str, "Style"]] = None
     915      ) -> "Text":
     916          """Add text with an optional style.
     917  
     918          Args:
     919              text (Union[Text, str]): A str or Text to append.
     920              style (str, optional): A style name. Defaults to None.
     921  
     922          Returns:
     923              Text: Returns self for chaining.
     924          """
     925  
     926          if not isinstance(text, (str, Text)):
     927              raise TypeError("Only str or Text can be appended to Text")
     928  
     929          if len(text):
     930              if isinstance(text, str):
     931                  sanitized_text = strip_control_codes(text)
     932                  self._text.append(sanitized_text)
     933                  offset = len(self)
     934                  text_length = len(sanitized_text)
     935                  if style is not None:
     936                      self._spans.append(Span(offset, offset + text_length, style))
     937                  self._length += text_length
     938              elif isinstance(text, Text):
     939                  _Span = Span
     940                  if style is not None:
     941                      raise ValueError(
     942                          "style must not be set when appending Text instance"
     943                      )
     944                  text_length = self._length
     945                  if text.style is not None:
     946                      self._spans.append(
     947                          _Span(text_length, text_length + len(text), text.style)
     948                      )
     949                  self._text.append(text.plain)
     950                  self._spans.extend(
     951                      _Span(start + text_length, end + text_length, style)
     952                      for start, end, style in text._spans
     953                  )
     954                  self._length += len(text)
     955          return self
     956  
     957      def append_text(self, text: "Text") -> "Text":
     958          """Append another Text instance. This method is more performant that Text.append, but
     959          only works for Text.
     960  
     961          Returns:
     962              Text: Returns self for chaining.
     963          """
     964          _Span = Span
     965          text_length = self._length
     966          if text.style is not None:
     967              self._spans.append(_Span(text_length, text_length + len(text), text.style))
     968          self._text.append(text.plain)
     969          self._spans.extend(
     970              _Span(start + text_length, end + text_length, style)
     971              for start, end, style in text._spans
     972          )
     973          self._length += len(text)
     974          return self
     975  
     976      def append_tokens(
     977          self, tokens: Iterable[Tuple[str, Optional[StyleType]]]
     978      ) -> "Text":
     979          """Append iterable of str and style. Style may be a Style instance or a str style definition.
     980  
     981          Args:
     982              pairs (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.
     983  
     984          Returns:
     985              Text: Returns self for chaining.
     986          """
     987          append_text = self._text.append
     988          append_span = self._spans.append
     989          _Span = Span
     990          offset = len(self)
     991          for content, style in tokens:
     992              append_text(content)
     993              if style is not None:
     994                  append_span(_Span(offset, offset + len(content), style))
     995              offset += len(content)
     996          self._length = offset
     997          return self
     998  
     999      def copy_styles(self, text: "Text") -> None:
    1000          """Copy styles from another Text instance.
    1001  
    1002          Args:
    1003              text (Text): A Text instance to copy styles from, must be the same length.
    1004          """
    1005          self._spans.extend(text._spans)
    1006  
    1007      def split(
    1008          self,
    1009          separator: str = "\n",
    1010          *,
    1011          include_separator: bool = False,
    1012          allow_blank: bool = False,
    1013      ) -> Lines:
    1014          """Split rich text in to lines, preserving styles.
    1015  
    1016          Args:
    1017              separator (str, optional): String to split on. Defaults to "\\\\n".
    1018              include_separator (bool, optional): Include the separator in the lines. Defaults to False.
    1019              allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.
    1020  
    1021          Returns:
    1022              List[RichText]: A list of rich text, one per line of the original.
    1023          """
    1024          assert separator, "separator must not be empty"
    1025  
    1026          text = self.plain
    1027          if separator not in text:
    1028              return Lines([self.copy()])
    1029  
    1030          if include_separator:
    1031              lines = self.divide(
    1032                  match.end() for match in re.finditer(re.escape(separator), text)
    1033              )
    1034          else:
    1035  
    1036              def flatten_spans() -> Iterable[int]:
    1037                  for match in re.finditer(re.escape(separator), text):
    1038                      start, end = match.span()
    1039                      yield start
    1040                      yield end
    1041  
    1042              lines = Lines(
    1043                  line for line in self.divide(flatten_spans()) if line.plain != separator
    1044              )
    1045  
    1046          if not allow_blank and text.endswith(separator):
    1047              lines.pop()
    1048  
    1049          return lines
    1050  
    1051      def divide(self, offsets: Iterable[int]) -> Lines:
    1052          """Divide text in to a number of lines at given offsets.
    1053  
    1054          Args:
    1055              offsets (Iterable[int]): Offsets used to divide text.
    1056  
    1057          Returns:
    1058              Lines: New RichText instances between offsets.
    1059          """
    1060          _offsets = list(offsets)
    1061  
    1062          if not _offsets:
    1063              return Lines([self.copy()])
    1064  
    1065          text = self.plain
    1066          text_length = len(text)
    1067          divide_offsets = [0, *_offsets, text_length]
    1068          line_ranges = list(zip(divide_offsets, divide_offsets[1:]))
    1069  
    1070          style = self.style
    1071          justify = self.justify
    1072          overflow = self.overflow
    1073          _Text = Text
    1074          new_lines = Lines(
    1075              _Text(
    1076                  text[start:end],
    1077                  style=style,
    1078                  justify=justify,
    1079                  overflow=overflow,
    1080              )
    1081              for start, end in line_ranges
    1082          )
    1083          if not self._spans:
    1084              return new_lines
    1085  
    1086          _line_appends = [line._spans.append for line in new_lines._lines]
    1087          line_count = len(line_ranges)
    1088          _Span = Span
    1089  
    1090          for span_start, span_end, style in self._spans:
    1091  
    1092              lower_bound = 0
    1093              upper_bound = line_count
    1094              start_line_no = (lower_bound + upper_bound) // 2
    1095  
    1096              while True:
    1097                  line_start, line_end = line_ranges[start_line_no]
    1098                  if span_start < line_start:
    1099                      upper_bound = start_line_no - 1
    1100                  elif span_start > line_end:
    1101                      lower_bound = start_line_no + 1
    1102                  else:
    1103                      break
    1104                  start_line_no = (lower_bound + upper_bound) // 2
    1105  
    1106              if span_end < line_end:
    1107                  end_line_no = start_line_no
    1108              else:
    1109                  end_line_no = lower_bound = start_line_no
    1110                  upper_bound = line_count
    1111  
    1112                  while True:
    1113                      line_start, line_end = line_ranges[end_line_no]
    1114                      if span_end < line_start:
    1115                          upper_bound = end_line_no - 1
    1116                      elif span_end > line_end:
    1117                          lower_bound = end_line_no + 1
    1118                      else:
    1119                          break
    1120                      end_line_no = (lower_bound + upper_bound) // 2
    1121  
    1122              for line_no in range(start_line_no, end_line_no + 1):
    1123                  line_start, line_end = line_ranges[line_no]
    1124                  new_start = max(0, span_start - line_start)
    1125                  new_end = min(span_end - line_start, line_end - line_start)
    1126                  if new_end > new_start:
    1127                      _line_appends[line_no](_Span(new_start, new_end, style))
    1128  
    1129          return new_lines
    1130  
    1131      def right_crop(self, amount: int = 1) -> None:
    1132          """Remove a number of characters from the end of the text."""
    1133          max_offset = len(self.plain) - amount
    1134          _Span = Span
    1135          self._spans[:] = [
    1136              (
    1137                  span
    1138                  if span.end < max_offset
    1139                  else _Span(span.start, min(max_offset, span.end), span.style)
    1140              )
    1141              for span in self._spans
    1142              if span.start < max_offset
    1143          ]
    1144          self._text = [self.plain[:-amount]]
    1145          self._length -= amount
    1146  
    1147      def wrap(
    1148          self,
    1149          console: "Console",
    1150          width: int,
    1151          *,
    1152          justify: Optional["JustifyMethod"] = None,
    1153          overflow: Optional["OverflowMethod"] = None,
    1154          tab_size: int = 8,
    1155          no_wrap: Optional[bool] = None,
    1156      ) -> Lines:
    1157          """Word wrap the text.
    1158  
    1159          Args:
    1160              console (Console): Console instance.
    1161              width (int): Number of characters per line.
    1162              emoji (bool, optional): Also render emoji code. Defaults to True.
    1163              justify (str, optional): Justify method: "default", "left", "center", "full", "right". Defaults to "default".
    1164              overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
    1165              tab_size (int, optional): Default tab size. Defaults to 8.
    1166              no_wrap (bool, optional): Disable wrapping, Defaults to False.
    1167  
    1168          Returns:
    1169              Lines: Number of lines.
    1170          """
    1171          wrap_justify = justify or self.justify or DEFAULT_JUSTIFY
    1172          wrap_overflow = overflow or self.overflow or DEFAULT_OVERFLOW
    1173  
    1174          no_wrap = pick_bool(no_wrap, self.no_wrap, False) or overflow == "ignore"
    1175  
    1176          lines = Lines()
    1177          for line in self.split(allow_blank=True):
    1178              if "\t" in line:
    1179                  line.expand_tabs(tab_size)
    1180              if no_wrap:
    1181                  new_lines = Lines([line])
    1182              else:
    1183                  offsets = divide_line(str(line), width, fold=wrap_overflow == "fold")
    1184                  new_lines = line.divide(offsets)
    1185              for line in new_lines:
    1186                  line.rstrip_end(width)
    1187              if wrap_justify:
    1188                  new_lines.justify(
    1189                      console, width, justify=wrap_justify, overflow=wrap_overflow
    1190                  )
    1191              for line in new_lines:
    1192                  line.truncate(width, overflow=wrap_overflow)
    1193              lines.extend(new_lines)
    1194          return lines
    1195  
    1196      def fit(self, width: int) -> Lines:
    1197          """Fit the text in to given width by chopping in to lines.
    1198  
    1199          Args:
    1200              width (int): Maximum characters in a line.
    1201  
    1202          Returns:
    1203              Lines: Lines container.
    1204          """
    1205          lines: Lines = Lines()
    1206          append = lines.append
    1207          for line in self.split():
    1208              line.set_length(width)
    1209              append(line)
    1210          return lines
    1211  
    1212      def detect_indentation(self) -> int:
    1213          """Auto-detect indentation of code.
    1214  
    1215          Returns:
    1216              int: Number of spaces used to indent code.
    1217          """
    1218  
    1219          _indentations = {
    1220              len(match.group(1))
    1221              for match in re.finditer(r"^( *)(.*)$", self.plain, flags=re.MULTILINE)
    1222          }
    1223  
    1224          try:
    1225              indentation = (
    1226                  reduce(gcd, [indent for indent in _indentations if not indent % 2]) or 1
    1227              )
    1228          except TypeError:
    1229              indentation = 1
    1230  
    1231          return indentation
    1232  
    1233      def with_indent_guides(
    1234          self,
    1235          indent_size: Optional[int] = None,
    1236          *,
    1237          character: str = "",
    1238          style: StyleType = "dim green",
    1239      ) -> "Text":
    1240          """Adds indent guide lines to text.
    1241  
    1242          Args:
    1243              indent_size (Optional[int]): Size of indentation, or None to auto detect. Defaults to None.
    1244              character (str, optional): Character to use for indentation. Defaults to "│".
    1245              style (Union[Style, str], optional): Style of indent guides.
    1246  
    1247          Returns:
    1248              Text: New text with indentation guides.
    1249          """
    1250  
    1251          _indent_size = self.detect_indentation() if indent_size is None else indent_size
    1252  
    1253          text = self.copy()
    1254          text.expand_tabs()
    1255          indent_line = f"{character}{' ' * (_indent_size - 1)}"
    1256  
    1257          re_indent = re.compile(r"^( *)(.*)$")
    1258          new_lines: List[Text] = []
    1259          add_line = new_lines.append
    1260          blank_lines = 0
    1261          for line in text.split(allow_blank=True):
    1262              match = re_indent.match(line.plain)
    1263              if not match or not match.group(2):
    1264                  blank_lines += 1
    1265                  continue
    1266              indent = match.group(1)
    1267              full_indents, remaining_space = divmod(len(indent), _indent_size)
    1268              new_indent = f"{indent_line * full_indents}{' ' * remaining_space}"
    1269              line.plain = new_indent + line.plain[len(new_indent) :]
    1270              line.stylize(style, 0, len(new_indent))
    1271              if blank_lines:
    1272                  new_lines.extend([Text(new_indent, style=style)] * blank_lines)
    1273                  blank_lines = 0
    1274              add_line(line)
    1275          if blank_lines:
    1276              new_lines.extend([Text("", style=style)] * blank_lines)
    1277  
    1278          new_text = text.blank_copy("\n").join(new_lines)
    1279          return new_text
    1280  
    1281  
    1282  if __name__ == "__main__":  # pragma: no cover
    1283      from pip._vendor.rich.console import Console
    1284  
    1285      text = Text(
    1286          """\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"""
    1287      )
    1288      text.highlight_words(["Lorem"], "bold")
    1289      text.highlight_words(["ipsum"], "italic")
    1290  
    1291      console = Console()
    1292  
    1293      console.rule("justify='left'")
    1294      console.print(text, style="red")
    1295      console.print()
    1296  
    1297      console.rule("justify='center'")
    1298      console.print(text, style="green", justify="center")
    1299      console.print()
    1300  
    1301      console.rule("justify='right'")
    1302      console.print(text, style="blue", justify="right")
    1303      console.print()
    1304  
    1305      console.rule("justify='full'")
    1306      console.print(text, style="magenta", justify="full")
    1307      console.print()