@@ -106,6 +106,27 @@ def extend(self, cells: int) -> "Span":
106106 return Span (start , end + cells , style )
107107 return self
108108
109+ def shift (self , distance : int ) -> "Span" :
110+ """Shift a span a given distance.
111+
112+ Note that the start offset is clamped to 0.
113+ The end offset is not clamped, as it is assumed this has already been checked by the caller.
114+
115+ Args:
116+ distance: Number of characters to move.
117+
118+ Returns:
119+ New Span.
120+ """
121+ if distance < 0 :
122+ start , end , style = self
123+ return Span (
124+ offset if (offset := start + distance ) > 0 else 0 , end + distance , style
125+ )
126+ else :
127+ start , end , style = self
128+ return Span (start + distance , end + distance , style )
129+
109130
110131@rich .repr .auto
111132@total_ordering
@@ -126,6 +147,7 @@ def __init__(
126147 text : str = "" ,
127148 spans : list [Span ] | None = None ,
128149 cell_length : int | None = None ,
150+ strip_control_codes : bool = True ,
129151 ) -> None :
130152 """
131153 Initialize a Content object.
@@ -134,8 +156,12 @@ def __init__(
134156 text: text content.
135157 spans: Optional list of spans.
136158 cell_length: Cell length of text if known, otherwise `None`.
159+ strip_control_codes: Strip control codes that may break output?
137160 """
138- self ._text : str = _strip_control_codes (text )
161+ if strip_control_codes and text :
162+ self ._text : str = _strip_control_codes (text )
163+ else :
164+ self ._text = text
139165 self ._spans : list [Span ] = [] if spans is None else spans
140166 self ._cell_length = cell_length
141167 self ._optimal_width_cache : int | None = None
@@ -147,6 +173,8 @@ def __init__(
147173 self ._split_cache : FIFOCache [tuple [str , bool , bool ], list [Content ]] | None = (
148174 None
149175 )
176+ # If there are 1 or 0 spans, it can't be simplified further
177+ self ._simplified = len (self ._spans ) <= 1
150178
151179 def __str__ (self ) -> str :
152180 return self ._text
@@ -321,25 +349,35 @@ def styled(
321349 text : str ,
322350 style : Style | str = "" ,
323351 cell_length : int | None = None ,
352+ strip_control_codes : bool = True ,
324353 ) -> Content :
325354 """Create a Content instance from text and an optional style.
326355
327356 Args:
328357 text: String content.
329358 style: Desired style.
330359 cell_length: Cell length of text if known, otherwise `None`.
360+ strip_control_codes: Strip control codes that may break output.
331361
332362 Returns:
333363 New Content instance.
334364 """
335365 if not text :
336366 return Content ("" )
337- new_content = cls (text , [Span (0 , len (text ), style )] if style else None )
367+ new_content = cls (
368+ text ,
369+ [Span (0 , len (text ), style )] if style else None ,
370+ cell_length ,
371+ strip_control_codes = strip_control_codes ,
372+ )
338373 return new_content
339374
340375 @classmethod
341376 def assemble (
342- cls , * parts : str | Content | tuple [str , str | Style ], end : str = ""
377+ cls ,
378+ * parts : str | Content | tuple [str , str | Style ],
379+ end : str = "" ,
380+ strip_control_codes : bool = True ,
343381 ) -> Content :
344382 """Construct new content from string, content, or tuples of (TEXT, STYLE).
345383
@@ -359,6 +397,7 @@ def assemble(
359397 *parts: Parts to join to gether. A *part* may be a simple string, another Content
360398 instance, or tuple containing text and a style.
361399 end: Optional end to the Content.
400+ strip_control_codes: Strip control codes that may break output.
362401 """
363402 text : list [str ] = []
364403 spans : list [Span ] = []
@@ -390,7 +429,7 @@ def assemble(
390429 position += len (part .plain )
391430 if end :
392431 text_append (end )
393- return cls ("" .join (text ), spans )
432+ return cls ("" .join (text ), spans , strip_control_codes = strip_control_codes )
394433
395434 def simplify (self ) -> Content :
396435 """Simplify spans by joining contiguous spans together.
@@ -405,7 +444,7 @@ def simplify(self) -> Content:
405444 Returns:
406445 Self.
407446 """
408- if not (spans := self ._spans ):
447+ if not (spans := self ._spans ) or self . _simplified :
409448 return self
410449 last_span = Span (- 1 , - 1 , "" )
411450 new_spans : list [Span ] = []
@@ -419,6 +458,7 @@ def simplify(self) -> Content:
419458 last_span = span
420459 if changed :
421460 self ._spans [:] = new_spans
461+ self ._simplified = True
422462 return self
423463
424464 def add_spans (self , spans : Sequence [Span ]) -> Content :
@@ -431,7 +471,12 @@ def add_spans(self, spans: Sequence[Span]) -> Content:
431471 A Content instance.
432472 """
433473 if spans :
434- return Content (self .plain , [* self ._spans , * spans ], self ._cell_length )
474+ return Content (
475+ self .plain ,
476+ [* self ._spans , * spans ],
477+ self ._cell_length ,
478+ strip_control_codes = False ,
479+ )
435480 return self
436481
437482 def __eq__ (self , other : object ) -> bool :
@@ -706,7 +751,7 @@ def plain(self) -> str:
706751 def without_spans (self ) -> Content :
707752 """The content with no spans"""
708753 if self ._spans :
709- return Content (self .plain , [], self ._cell_length )
754+ return Content (self .plain , [], self ._cell_length , strip_control_codes = False )
710755 return self
711756
712757 @property
@@ -726,6 +771,7 @@ def get_text_at(offset: int) -> "Content":
726771 for start , end , style in self ._spans
727772 if end > offset >= start
728773 ],
774+ strip_control_codes = False ,
729775 )
730776 return content
731777
@@ -734,16 +780,32 @@ def get_text_at(offset: int) -> "Content":
734780 else :
735781 start , stop , step = slice .indices (len (self .plain ))
736782 if step == 1 :
737- lines = self .divide ([start , stop ])
738- return lines [1 ]
783+ if start == 0 :
784+ if stop >= len (self .plain ):
785+ return self
786+ text = self .plain [:stop ]
787+ return Content (
788+ text ,
789+ self ._trim_spans (text , self ._spans ),
790+ strip_control_codes = False ,
791+ )
792+ else :
793+ text = self .plain [start :stop ]
794+ spans = [
795+ span .shift (- start ) for span in self ._spans if span .end > start
796+ ]
797+ return Content (
798+ text , self ._trim_spans (text , spans ), strip_control_codes = False
799+ )
800+
739801 else :
740802 # This would be a bit of work to implement efficiently
741803 # For now, its not required
742804 raise TypeError ("slices with step!=1 are not supported" )
743805
744806 def __add__ (self , other : Content | str ) -> Content :
745807 if isinstance (other , str ):
746- return Content (self ._text + other , self ._spans )
808+ return Content (self ._text + other , self ._spans , strip_control_codes = False )
747809 if isinstance (other , Content ):
748810 offset = len (self .plain )
749811 content = Content (
@@ -801,6 +863,7 @@ def append(self, content: Content | str) -> Content:
801863 if self ._cell_length is None
802864 else self ._cell_length + cell_len (content )
803865 ),
866+ strip_control_codes = False ,
804867 )
805868 return Content ("" ).join ([self , content ])
806869
@@ -836,12 +899,20 @@ def iter_content() -> Iterable[Content]:
836899 """Iterate the lines, optionally inserting the separator."""
837900 if self .plain :
838901 for last , line in loop_last (lines ):
839- yield line if isinstance (line , Content ) else Content (line )
902+ yield (
903+ line
904+ if isinstance (line , Content )
905+ else Content (line , strip_control_codes = False )
906+ )
840907 if not last :
841908 yield self
842909 else :
843910 for line in lines :
844- yield line if isinstance (line , Content ) else Content (line )
911+ yield (
912+ line
913+ if isinstance (line , Content )
914+ else Content (line , strip_control_codes = False )
915+ )
845916
846917 extend_text = text .extend
847918 extend_spans = spans .extend
@@ -935,13 +1006,16 @@ def truncate(
9351006 if pad and length < max_width :
9361007 spaces = max_width - length
9371008 text = f"{ self .plain } { ' ' * spaces } "
1009+ return Content (text , spans , max_width , strip_control_codes = False )
9381010 elif length > max_width :
9391011 if ellipsis and max_width :
9401012 text = set_cell_size (self .plain , max_width - 1 ) + "…"
9411013 else :
9421014 text = set_cell_size (self .plain , max_width )
9431015 spans = self ._trim_spans (text , self ._spans )
944- return Content (text , spans )
1016+ return Content (text , spans , max_width , strip_control_codes = False )
1017+ else :
1018+ return self
9451019
9461020 def pad_left (self , count : int , character : str = " " ) -> Content :
9471021 """Pad the left with a given character.
@@ -962,6 +1036,7 @@ def pad_left(self, count: int, character: str = " ") -> Content:
9621036 text ,
9631037 spans ,
9641038 None if self ._cell_length is None else self ._cell_length + count ,
1039+ strip_control_codes = False ,
9651040 )
9661041 return content
9671042
@@ -987,6 +1062,7 @@ def extend_right(self, count: int, character: str = " ") -> Content:
9871062 for span in self ._spans
9881063 ],
9891064 None if self ._cell_length is None else self ._cell_length + count ,
1065+ strip_control_codes = False ,
9901066 )
9911067 return self
9921068
@@ -1003,6 +1079,7 @@ def pad_right(self, count: int, character: str = " ") -> Content:
10031079 f"{ self .plain } { character * count } " ,
10041080 self ._spans ,
10051081 None if self ._cell_length is None else self ._cell_length + count ,
1082+ strip_control_codes = False ,
10061083 )
10071084 return self
10081085
@@ -1029,6 +1106,7 @@ def pad(self, left: int, right: int, character: str = " ") -> Content:
10291106 text ,
10301107 spans ,
10311108 None if self ._cell_length is None else self ._cell_length + left + right ,
1109+ strip_control_codes = False ,
10321110 )
10331111 return content
10341112
@@ -1114,6 +1192,8 @@ def stylize(
11141192 return Content (
11151193 self .plain ,
11161194 self ._spans + [Span (start , length if length < end else end , style )],
1195+ self ._cell_length ,
1196+ strip_control_codes = False ,
11171197 )
11181198
11191199 def stylize_before (
@@ -1146,6 +1226,8 @@ def stylize_before(
11461226 return Content (
11471227 self .plain ,
11481228 [Span (start , length if length < end else end , style ), * self ._spans ],
1229+ self ._cell_length ,
1230+ strip_control_codes = False ,
11491231 )
11501232
11511233 def render (
0 commit comments