2 from functools
import partial
, reduce
4 from operator
import itemgetter
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
31 if TYPE_CHECKING
: # pragma: no cover
32 from .console
import Console
, ConsoleOptions
, JustifyMethod
, OverflowMethod
34 DEFAULT_JUSTIFY
: "JustifyMethod" = "default"
35 DEFAULT_OVERFLOW
: "OverflowMethod" = "fold"
38 _re_whitespace
= re
.compile(r
"\s+$")
40 TextType
= Union
[str, "Text"]
42 GetStyleCallable
= Callable
[[str], Optional
[StyleType
]]
45 class Span(NamedTuple
):
46 """A marked up region in some text."""
49 """Span start index."""
52 style
: Union
[str, Style
]
53 """Style associated with the span."""
55 def __repr__(self
) -> str:
56 return f
"Span({self.start}, {self.end}, {self.style!r})"
58 def __bool__(self
) -> bool:
59 return self
.end
> self
.start
61 def split(self
, offset
: int) -> Tuple
["Span", Optional
["Span"]]:
62 """Split a span in to 2 from a given offset."""
64 if offset
< self
.start
:
66 if offset
>= self
.end
:
69 start
, end
, style
= self
70 span1
= Span(start
, min(end
, offset
), style
)
71 span2
= Span(span1
.end
, end
, style
)
74 def move(self
, offset
: int) -> "Span":
75 """Move start and end by a given offset.
78 offset (int): Number of characters to add to start and end.
81 TextSpan: A new TextSpan with adjusted position.
83 start
, end
, style
= self
84 return Span(start
+ offset
, end
+ offset
, style
)
86 def right_crop(self
, offset
: int) -> "Span":
87 """Crop the span at the given offset.
90 offset (int): A value between start and end.
93 Span: A new (possibly smaller) span.
95 start
, end
, style
= self
98 return Span(start
, min(offset
, end
), style
)
101 class Text(JupyterMixin
):
102 """Text with color / style.
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.
130 style
: Union
[str, Style
] = "",
132 justify
: Optional
["JustifyMethod"] = None,
133 overflow
: Optional
["OverflowMethod"] = None,
134 no_wrap
: Optional
[bool] = None,
136 tab_size
: Optional
[int] = 8,
137 spans
: Optional
[List
[Span
]] = None,
139 sanitized_text
= strip_control_codes(text
)
140 self
._text
= [sanitized_text
]
142 self
.justify
: Optional
["JustifyMethod"] = justify
143 self
.overflow
: Optional
["OverflowMethod"] = overflow
144 self
.no_wrap
= no_wrap
146 self
.tab_size
= tab_size
147 self
._spans
: List
[Span
] = spans
or []
148 self
._length
: int = len(sanitized_text
)
150 def __len__(self
) -> int:
153 def __bool__(self
) -> bool:
154 return bool(self
._length
)
156 def __str__(self
) -> str:
159 def __repr__(self
) -> str:
160 return f
"<text {self.plain!r} {self._spans!r}>"
162 def __add__(self
, other
: Any
) -> "Text":
163 if isinstance(other
, (str, Text
)):
167 return NotImplemented
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
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
181 def __getitem__(self
, slice: Union
[int, slice]) -> "Text":
182 def get_text_at(offset
: int) -> "Text":
188 for start
, end
, style
in self
._spans
189 if end
> offset
>= start
195 if isinstance(slice, int):
196 return get_text_at(slice)
198 start
, stop
, step
= slice.indices(len(self
.plain
))
200 lines
= self
.divide([start
, stop
])
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")
208 def cell_len(self
) -> int:
209 """Get the number of cells required to render this text."""
210 return cell_len(self
.plain
)
213 def markup(self
) -> str:
214 """Get console markup to render this Text.
217 str: A string potentially creating markup tags.
219 from .markup
import escape
221 output
: List
[str] = []
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
),
230 markup_spans
.sort(key
=itemgetter(0, 1))
232 append
= output
.append
233 for offset
, closing
, style
in markup_spans
:
234 if offset
> position
:
235 append(escape(plain
[position
:offset
]))
238 append(f
"[/{style}]" if closing
else f
"[{style}]")
239 markup
= "".join(output
)
247 style
: Union
[str, Style
] = "",
249 emoji_variant
: Optional
[EmojiVariant
] = None,
250 justify
: Optional
["JustifyMethod"] = None,
251 overflow
: Optional
["OverflowMethod"] = None,
254 """Create Text instance from markup.
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".
264 Text: A Text instance with markup rendered.
266 from .markup
import render
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
279 style
: Union
[str, Style
] = "",
280 justify
: Optional
["JustifyMethod"] = None,
281 overflow
: Optional
["OverflowMethod"] = None,
282 no_wrap
: Optional
[bool] = None,
284 tab_size
: Optional
[int] = 8,
286 """Create a Text object from a string containing ANSI escape codes.
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.
297 from .ansi
import AnsiDecoder
308 decoder
= AnsiDecoder()
309 result
= joiner
.join(line
for line
in decoder
.decode(text
))
316 style
: StyleType
= "",
318 justify
: Optional
["JustifyMethod"] = None,
319 overflow
: Optional
["OverflowMethod"] = None,
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.
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.
331 Text: A text instance with a style applied to the entire string.
333 styled_text
= cls(text
, justify
=justify
, overflow
=overflow
)
334 styled_text
.stylize(style
)
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,
347 meta
: Optional
[Dict
[str, Any
]] = None,
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.
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
361 Text: A new text instance.
374 if isinstance(part
, (_Text
, str)):
379 text
.apply_meta(meta
)
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
)]
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
:
401 def spans(self
) -> List
[Span
]:
402 """Get a reference to the internal list of spans."""
406 def spans(self
, spans
: List
[Span
]) -> None:
408 self
._spans
= spans
[:]
410 def blank_copy(self
, plain
: str = "") -> "Text":
411 """Return a new Text instance with copied meta data (but not the string or spans)."""
415 justify
=self
.justify
,
416 overflow
=self
.overflow
,
417 no_wrap
=self
.no_wrap
,
419 tab_size
=self
.tab_size
,
423 def copy(self
) -> "Text":
424 """Return a copy of this instance."""
428 justify
=self
.justify
,
429 overflow
=self
.overflow
,
430 no_wrap
=self
.no_wrap
,
432 tab_size
=self
.tab_size
,
434 copy_self
._spans
[:] = self
._spans
439 style
: Union
[str, Style
],
441 end
: Optional
[int] = None,
443 """Apply a style to the text, or a portion of the text.
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.
453 start
= length
+ start
458 if start
>= length
or end
<= start
:
459 # Span not in text or not valid
461 self
._spans
.append(Span(start
, min(length
, end
), style
))
465 style
: Union
[str, Style
],
467 end
: Optional
[int] = None,
469 """Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.
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.
479 start
= length
+ start
484 if start
>= length
or end
<= start
:
485 # Span not in text or not valid
487 self
._spans
.insert(0, Span(start
, min(length
, end
), style
))
490 self
, meta
: Dict
[str, Any
], start
: int = 0, end
: Optional
[int] = None
492 """Apply meta data to the text, or a portion of the text.
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.
500 style
= Style
.from_meta(meta
)
501 self
.stylize(style
, start
=start
, end
=end
)
503 def on(self
, meta
: Optional
[Dict
[str, Any
]] = None, **handlers
: Any
) -> "Text":
504 """Apply event handlers (used by Textual project).
507 >>> from rich.text import Text
508 >>> text = Text("hello world")
509 >>> text.on(click="view.toggle('world')")
512 meta (Dict[str, Any]): Mapping of meta information.
513 **handlers: Keyword args are prefixed with "@" to defined handlers.
516 Text: Self is returned to method may be chained.
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))
523 def remove_suffix(self, suffix: str) -> None:
524 """Remove a suffix if it exists.
527 suffix (str): Suffix to remove.
529 if self.plain.endswith(suffix):
530 self.right_crop(len(suffix))
532 def get_style_at_offset(self, console: "Console
", offset: int) -> Style:
533 """Get the style of a character at give offset.
536 console (~Console): Console where text will be rendered.
537 offset (int): Offset in to text (negative indexing supported)
540 Style: A Style instance.
542 # TODO: This is a little inefficient, it is only used by full justify
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="")
555 style: Optional[Union[GetStyleCallable, StyleType]] = None,
557 style_prefix: str = "",
559 """Highlight text with a regular expression, where group names are
560 translated to styles.
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.
569 int: Number of regex matches
572 append_span = self._spans.append
575 for match in re.finditer(re_highlight, plain):
576 get_span = match.span
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))
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}
"))
592 words: Iterable[str],
593 style: Union[str, Style],
595 case_sensitive: bool = True,
597 """Highlight words with a style.
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.
605 int: Number of words highlighted.
607 re_words = "|
".join(re.escape(word) for word in words)
608 add_span = self._spans.append
611 for match in re.finditer(
612 re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE
614 start, end = match.span(0)
615 add_span(_Span(start, end, style))
619 def rstrip(self) -> None:
620 """Strip whitespace from end of text."""
621 self.plain = self.plain.rstrip()
623 def rstrip_end(self, size: int) -> None:
624 """Remove whitespace beyond a certain width at the end of the text.
627 size (int): The desired size of the text.
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))
637 def set_length(self, new_length: int) -> None:
638 """Set new length of the text, clipping or padding is required."""
640 if length != new_length:
641 if length < new_length:
642 self.pad_right(new_length - length)
644 self.right_crop(length - new_length)
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
652 overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW
659 tab_size=tab_size or 8,
660 no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),
662 all_lines = Text("\n").join(lines)
663 yield from all_lines.render(console, end=self.end)
665 def __rich_measure__(
666 self, console: "Console
", options: "ConsoleOptions
"
669 lines = text.splitlines()
670 max_text_width = max(cell_len(line) for line in lines) if lines else 0
673 max(cell_len(word) for word in words) if words else max_text_width
675 return Measurement(min_text_width, max_text_width)
677 def render(self, console: "Console
", end: str = "") -> Iterable["Segment
"]:
678 """Render the text as Segments.
681 console (Console): Console instance.
682 end (Optional[str], optional): Optional end character.
685 Iterable[Segment]: Result of render that may be written to the console.
694 get_style = partial(console.get_style, default=Style.null())
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)
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),
706 spans.sort(key=itemgetter(0, 1))
708 stack: List[int] = []
709 stack_append = stack.append
710 stack_pop = stack.remove
712 style_cache: Dict[Tuple[Style, ...], Style] = {}
713 style_cache_get = style_cache.get
714 combine = Style.combine
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:
722 current_style = combine(styles)
723 style_cache[styles] = current_style
726 for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
730 stack_append(style_id)
731 if next_offset > offset:
732 yield _Segment(text[offset:next_offset], get_current_style())
736 def join(self, lines: Iterable["Text
"]) -> "Text
":
737 """Join text together with this instance as the separator.
740 lines (Iterable[Text]): An iterable of Text instances to join.
743 Text: A new text instance containing join text.
746 new_text = self.blank_copy()
748 def iter_text() -> Iterable["Text
"]:
750 for last, line in loop_last(lines):
757 extend_text = new_text._text.extend
758 append_span = new_text._spans.append
759 extend_spans = new_text._spans.extend
763 for text in iter_text():
764 extend_text(text._text)
766 append_span(_Span(offset, offset + len(text), text.style))
768 _Span(offset + start, offset + end, style)
769 for start, end, style in text._spans
772 new_text._length = offset
775 def expand_tabs(self, tab_size: Optional[int] = None) -> None:
776 """Converts tabs to spaces.
779 tab_size (int, optional): Size of tabs. Defaults to 8.
782 if "\t" not in self.plain:
786 tab_size = self.tab_size
787 assert tab_size is not None
788 result = self.blank_copy()
789 append = result.append
792 for line in self.split("\n", include_separator=True):
793 parts = line.split("\t", include_separator=True)
795 if part.plain.endswith("\t"):
796 part._text = [part.plain[:-1] + " "]
799 spaces = tab_size - ((pos - 1) % tab_size) - 1
801 append(" " * spaces, _style)
805 self._text = [result.plain]
806 self._length = len(self.plain)
807 self._spans[:] = result._spans
813 overflow: Optional["OverflowMethod
"] = None,
816 """Truncate text if it is longer that a given width.
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.
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) + "…
"
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)
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)
843 if span.end < max_offset
844 else _Span(span.start, min(max_offset, span.end), span.style)
846 for span in self._spans
847 if span.start < max_offset
850 def pad(self, count: int, character: str = " ") -> None:
851 """Pad left and right with a given number of characters.
854 count (int): Width of padding.
856 assert len(character) == 1, "Character must be a string of length
1"
858 pad_characters = character * count
859 self.plain = f"{pad_characters}{self.plain}{pad_characters}
"
862 _Span(start + count, end + count, style)
863 for start, end, style in self._spans
866 def pad_left(self, count: int, character: str = " ") -> None:
867 """Pad the left with a given character.
870 count (int): Number of characters to pad.
871 character (str, optional): Character to pad with. Defaults to " ".
873 assert len(character) == 1, "Character must be a string of length
1"
875 self.plain = f"{character * count}{self.plain}
"
878 _Span(start + count, end + count, style)
879 for start, end, style in self._spans
882 def pad_right(self, count: int, character: str = " ") -> None:
883 """Pad the right with a given character.
886 count (int): Number of characters to pad.
887 character (str, optional): Character to pad with. Defaults to " ".
889 assert len(character) == 1, "Character must be a string of length
1"
891 self.plain = f"{self.plain}{character * count}
"
893 def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
894 """Align text to a given width.
897 align (AlignMethod): One of "left
", "center
", or "right
".
898 width (int): Desired width.
899 character (str, optional): Character to pad with. Defaults to " ".
902 excess_space = width - cell_len(self.plain)
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)
911 self.pad_left(excess_space, character)
914 self, text: Union["Text
", str], style: Optional[Union[str, "Style
"]] = None
916 """Add text with an optional style.
919 text (Union[Text, str]): A str or Text to append.
920 style (str, optional): A style name. Defaults to None.
923 Text: Returns self for chaining.
926 if not isinstance(text, (str, Text)):
927 raise TypeError("Only
str or Text can be appended to Text
")
930 if isinstance(text, str):
931 sanitized_text = strip_control_codes(text)
932 self._text.append(sanitized_text)
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):
940 if style is not None:
942 "style must
not be
set when appending Text instance
"
944 text_length = self._length
945 if text.style is not None:
947 _Span(text_length, text_length + len(text), text.style)
949 self._text.append(text.plain)
951 _Span(start + text_length, end + text_length, style)
952 for start, end, style in text._spans
954 self._length += len(text)
957 def append_text(self, text: "Text
") -> "Text
":
958 """Append another Text instance. This method is more performant that Text.append, but
962 Text: Returns self for chaining.
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)
970 _Span(start + text_length, end + text_length, style)
971 for start, end, style in text._spans
973 self._length += len(text)
977 self, tokens: Iterable[Tuple[str, Optional[StyleType]]]
979 """Append iterable of str and style. Style may be a Style instance or a str style definition.
982 pairs (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.
985 Text: Returns self for chaining.
987 append_text = self._text.append
988 append_span = self._spans.append
991 for content, style in tokens:
993 if style is not None:
994 append_span(_Span(offset, offset + len(content), style))
995 offset += len(content)
996 self._length = offset
999 def copy_styles(self, text: "Text
") -> None:
1000 """Copy styles from another Text instance.
1003 text (Text): A Text instance to copy styles from, must be the same length.
1005 self._spans.extend(text._spans)
1009 separator: str = "\n",
1011 include_separator: bool = False,
1012 allow_blank: bool = False,
1014 """Split rich text in to lines, preserving styles.
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.
1022 List[RichText]: A list of rich text, one per line of the original.
1024 assert separator, "separator must
not be empty
"
1027 if separator not in text:
1028 return Lines([self.copy()])
1030 if include_separator:
1031 lines = self.divide(
1032 match.end() for match in re.finditer(re.escape(separator), text)
1036 def flatten_spans() -> Iterable[int]:
1037 for match in re.finditer(re.escape(separator), text):
1038 start, end = match.span()
1043 line for line in self.divide(flatten_spans()) if line.plain != separator
1046 if not allow_blank and text.endswith(separator):
1051 def divide(self, offsets: Iterable[int]) -> Lines:
1052 """Divide text in to a number of lines at given offsets.
1055 offsets (Iterable[int]): Offsets used to divide text.
1058 Lines: New RichText instances between offsets.
1060 _offsets = list(offsets)
1063 return Lines([self.copy()])
1066 text_length = len(text)
1067 divide_offsets = [0, *_offsets, text_length]
1068 line_ranges = list(zip(divide_offsets, divide_offsets[1:]))
1071 justify = self.justify
1072 overflow = self.overflow
1081 for start, end in line_ranges
1086 _line_appends = [line._spans.append for line in new_lines._lines]
1087 line_count = len(line_ranges)
1090 for span_start, span_end, style in self._spans:
1093 upper_bound = line_count
1094 start_line_no = (lower_bound + upper_bound) // 2
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
1104 start_line_no = (lower_bound + upper_bound) // 2
1106 if span_end < line_end:
1107 end_line_no = start_line_no
1109 end_line_no = lower_bound = start_line_no
1110 upper_bound = line_count
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
1120 end_line_no = (lower_bound + upper_bound) // 2
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))
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
1138 if span.end < max_offset
1139 else _Span(span.start, min(max_offset, span.end), span.style)
1141 for span in self._spans
1142 if span.start < max_offset
1144 self._text = [self.plain[:-amount]]
1145 self._length -= amount
1152 justify: Optional["JustifyMethod
"] = None,
1153 overflow: Optional["OverflowMethod
"] = None,
1155 no_wrap: Optional[bool] = None,
1157 """Word wrap the text.
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.
1169 Lines: Number of lines.
1171 wrap_justify = justify or self.justify or DEFAULT_JUSTIFY
1172 wrap_overflow = overflow or self.overflow or DEFAULT_OVERFLOW
1174 no_wrap = pick_bool(no_wrap, self.no_wrap, False) or overflow == "ignore
"
1177 for line in self.split(allow_blank=True):
1179 line.expand_tabs(tab_size)
1181 new_lines = Lines([line])
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)
1189 console, width, justify=wrap_justify, overflow=wrap_overflow
1191 for line in new_lines:
1192 line.truncate(width, overflow=wrap_overflow)
1193 lines.extend(new_lines)
1196 def fit(self, width: int) -> Lines:
1197 """Fit the text in to given width by chopping in to lines.
1200 width (int): Maximum characters in a line.
1203 Lines: Lines container.
1205 lines: Lines = Lines()
1206 append = lines.append
1207 for line in self.split():
1208 line.set_length(width)
1212 def detect_indentation(self) -> int:
1213 """Auto-detect indentation of code.
1216 int: Number of spaces used to indent code.
1221 for match in re.finditer(r"^
( *)(.*)$
", self.plain, flags=re.MULTILINE)
1226 reduce(gcd, [indent for indent in _indentations if not indent % 2]) or 1
1233 def with_indent_guides(
1235 indent_size: Optional[int] = None,
1237 character: str = "│
",
1238 style: StyleType = "dim green
",
1240 """Adds indent guide lines to text.
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.
1248 Text: New text with indentation guides.
1251 _indent_size = self.detect_indentation() if indent_size is None else indent_size
1255 indent_line = f"{character}{' ' * (_indent_size - 1)}
"
1257 re_indent = re.compile(r"^
( *)(.*)$
")
1258 new_lines: List[Text] = []
1259 add_line = new_lines.append
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):
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))
1272 new_lines.extend([Text(new_indent, style=style)] * blank_lines)
1276 new_lines.extend([Text("", style=style)] * blank_lines)
1278 new_text = text.blank_copy("\n").join(new_lines)
1282 if __name__ == "__main__
": # pragma: no cover
1283 from pip._vendor.rich.console import Console
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"""
1288 text.highlight_words(["Lorem
"], "bold
")
1289 text.highlight_words(["ipsum
"], "italic
")
1293 console.rule("justify
='left'")
1294 console.print(text, style="red
")
1297 console.rule("justify
='center'")
1298 console.print(text, style="green
", justify="center
")
1301 console.rule("justify
='right'")
1302 console.print(text, style="blue
", justify="right
")
1305 console.rule("justify
='full'")
1306 console.print(text, style="magenta
", justify="full
")