]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/pip/_vendor/rich/text.py
init: venv aand flask
[dlqueue.git] / venv / 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 Span(NamedTuple):
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 Text(JupyterMixin):
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()