Class: Rich::Text
- Inherits:
-
Object
- Object
- Rich::Text
- Defined in:
- lib/rich/text.rb
Overview
Rich text with style spans. Text objects contain plain text plus a list of style spans that define how different portions of the text should be rendered.
Instance Attribute Summary collapse
-
#end ⇒ Boolean
readonly
End with newline.
-
#justify ⇒ Symbol
readonly
Justification (:left, :center, :right, :full).
-
#no_wrap ⇒ Boolean
readonly
No wrap.
-
#overflow ⇒ Symbol
readonly
Overflow handling (:fold, :crop, :ellipsis).
-
#plain ⇒ String
readonly
Plain text content.
-
#spans ⇒ Array<Span>
readonly
Style spans.
-
#style ⇒ Style?
readonly
Base style for entire text.
Class Method Summary collapse
-
.assemble(*parts) ⇒ Text
Assemble text from multiple parts.
-
.from_markup(markup) ⇒ Text
Create from markup.
-
.styled(content, style) ⇒ Text
Create styled text.
Instance Method Summary collapse
-
#append(text, style: nil) ⇒ self
(also: #<<)
Append text with optional style.
-
#append_text(other) ⇒ self
Append a Text object.
-
#cell_length ⇒ Integer
Cell width of text.
-
#copy ⇒ Object
Copy the text object.
-
#copy_spans_into(target_line, src_start, src_len, line_offset) ⇒ void
Copy any spans overlapping the source range [src_start, src_start+src_len) onto
target_line, shifted to begin atline_offsetwithin that line. - #dup ⇒ Object
-
#empty? ⇒ Boolean
True if text is empty.
-
#highlight_regex(re, style:) ⇒ Object
Highlight occurrences matching a regex.
-
#highlight_words(words, style:) ⇒ Object
Highlight occurrences of words.
-
#initialize(text = "", style: nil, justify: :left, overflow: :fold, no_wrap: false, end_str: "\n") ⇒ Text
constructor
Create new Text.
- #inspect ⇒ Object
-
#length ⇒ Integer
Length of text.
-
#render(color_system: ColorSystem::TRUECOLOR) ⇒ String
Render text to string with ANSI codes.
-
#slice(start_pos, length = nil) ⇒ Text
Get a substring as a new Text object.
-
#split(delimiter = "\n") ⇒ Array<Text>
Split text by a delimiter.
-
#stylize(style, start_pos = 0, end_pos = nil) ⇒ self
Apply a style to a range.
-
#stylize_all(style) ⇒ self
Apply a style to the entire text.
- #to_s ⇒ String
-
#to_segments ⇒ Array<Segment>
Convert to segments for rendering.
-
#wrap(width) ⇒ Array<Text>
Wrap text to a given width.
Constructor Details
#initialize(text = "", style: nil, justify: :left, overflow: :fold, no_wrap: false, end_str: "\n") ⇒ Text
Create new Text
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rich/text.rb', line 102 def initialize( text = "", style: nil, justify: :left, overflow: :fold, no_wrap: false, end_str: "\n" ) @plain = +text.to_s @spans = [] @style = style.is_a?(String) ? Style.parse(style) : style @justify = justify @overflow = overflow @no_wrap = no_wrap @end = end_str end |
Instance Attribute Details
#end ⇒ Boolean (readonly)
Returns End with newline.
94 95 96 |
# File 'lib/rich/text.rb', line 94 def end @end end |
#justify ⇒ Symbol (readonly)
Returns Justification (:left, :center, :right, :full).
85 86 87 |
# File 'lib/rich/text.rb', line 85 def justify @justify end |
#no_wrap ⇒ Boolean (readonly)
Returns No wrap.
91 92 93 |
# File 'lib/rich/text.rb', line 91 def no_wrap @no_wrap end |
#overflow ⇒ Symbol (readonly)
Returns Overflow handling (:fold, :crop, :ellipsis).
88 89 90 |
# File 'lib/rich/text.rb', line 88 def overflow @overflow end |
#plain ⇒ String (readonly)
Returns Plain text content.
76 77 78 |
# File 'lib/rich/text.rb', line 76 def plain @plain end |
#spans ⇒ Array<Span> (readonly)
Returns Style spans.
79 80 81 |
# File 'lib/rich/text.rb', line 79 def spans @spans end |
#style ⇒ Style? (readonly)
Returns Base style for entire text.
82 83 84 |
# File 'lib/rich/text.rb', line 82 def style @style end |
Class Method Details
.assemble(*parts) ⇒ Text
Assemble text from multiple parts
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/rich/text.rb', line 415 def assemble(*parts) text = Text.new parts.each do |part| case part when String text.append(part) when Array content, style = part text.append(content, style: style) when Text text.append_text(part) end end text end |
Instance Method Details
#append(text, style: nil) ⇒ self Also known as: <<
Append text with optional style
138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/rich/text.rb', line 138 def append(text, style: nil) if text.is_a?(Text) append_text(text) else start_pos = @plain.length @plain << text.to_s if style parsed_style = style.is_a?(String) ? Style.parse(style) : style @spans << Span.new(start_pos, @plain.length, parsed_style) end end self end |
#append_text(other) ⇒ self
Append a Text object
158 159 160 161 162 163 164 165 166 167 |
# File 'lib/rich/text.rb', line 158 def append_text(other) offset = @plain.length @plain << other.plain other.spans.each do |span| @spans << Span.new(span.start + offset, span.end + offset, span.style) end self end |
#cell_length ⇒ Integer
Returns Cell width of text.
125 126 127 |
# File 'lib/rich/text.rb', line 125 def cell_length Cells.cached_cell_len(@plain) end |
#copy ⇒ Object
Copy the text object
330 331 332 |
# File 'lib/rich/text.rb', line 330 def copy dup end |
#copy_spans_into(target_line, src_start, src_len, line_offset) ⇒ void
This method returns an undefined value.
Copy any spans overlapping the source range [src_start, src_start+src_len) onto target_line, shifted to begin at line_offset within that line.
295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/rich/text.rb', line 295 def copy_spans_into(target_line, src_start, src_len, line_offset) return if src_len <= 0 src_end = src_start + src_len @spans.each do |span| next unless span.overlaps?(src_start, src_end) new_start = [span.start - src_start, 0].max + line_offset new_end = [span.end - src_start, src_len].min + line_offset target_line.spans << Span.new(new_start, new_end, span.style) if new_end > new_start end end |
#dup ⇒ Object
398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/rich/text.rb', line 398 def dup new_text = Text.new( @plain.dup, style: @style, justify: @justify, overflow: @overflow, no_wrap: @no_wrap, end_str: @end ) @spans.each { |span| new_text.spans << span } new_text end |
#empty? ⇒ Boolean
Returns True if text is empty.
130 131 132 |
# File 'lib/rich/text.rb', line 130 def empty? @plain.empty? end |
#highlight_regex(re, style:) ⇒ Object
Highlight occurrences matching a regex
321 322 323 324 325 326 327 |
# File 'lib/rich/text.rb', line 321 def highlight_regex(re, style:) @plain.scan(re) do match = Regexp.last_match stylize(style, match.begin(0), match.end(0)) end self end |
#highlight_words(words, style:) ⇒ Object
Highlight occurrences of words
309 310 311 312 313 314 315 316 317 318 |
# File 'lib/rich/text.rb', line 309 def highlight_words(words, style:) words.each do |word| pos = 0 while (pos = @plain.index(word, pos)) stylize(style, pos, pos + word.length) pos += word.length end end self end |
#inspect ⇒ Object
394 395 396 |
# File 'lib/rich/text.rb', line 394 def inspect "#<Rich::Text #{@plain.inspect} spans=#{@spans.length}>" end |
#length ⇒ Integer
Returns Length of text.
120 121 122 |
# File 'lib/rich/text.rb', line 120 def length @plain.length end |
#render(color_system: ColorSystem::TRUECOLOR) ⇒ String
Render text to string with ANSI codes
385 386 387 |
# File 'lib/rich/text.rb', line 385 def render(color_system: ColorSystem::TRUECOLOR) Segment.render(to_segments, color_system: color_system) end |
#slice(start_pos, length = nil) ⇒ Text
Get a substring as a new Text object
194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/rich/text.rb', line 194 def slice(start_pos, length = nil) end_pos = length ? start_pos + length : @plain.length new_text = Text.new(@plain[start_pos...end_pos], style: @style) @spans.each do |span| next unless span.overlaps?(start_pos, end_pos) new_start = [span.start - start_pos, 0].max new_end = [span.end - start_pos, end_pos - start_pos].min new_text.spans << Span.new(new_start, new_end, span.style) end new_text end |
#split(delimiter = "\n") ⇒ Array<Text>
Split text by a delimiter
212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/rich/text.rb', line 212 def split(delimiter = "\n") parts = [] pos = 0 @plain.split(delimiter, -1).each do |part| end_pos = pos + part.length parts << slice(pos, part.length) pos = end_pos + delimiter.length end parts end |
#stylize(style, start_pos = 0, end_pos = nil) ⇒ self
Apply a style to a range
174 175 176 177 178 179 180 181 |
# File 'lib/rich/text.rb', line 174 def stylize(style, start_pos = 0, end_pos = nil) end_pos ||= @plain.length return self if start_pos >= end_pos parsed_style = style.is_a?(String) ? Style.parse(style) : style @spans << Span.new(start_pos, end_pos, parsed_style) self end |
#stylize_all(style) ⇒ self
Apply a style to the entire text
186 187 188 |
# File 'lib/rich/text.rb', line 186 def stylize_all(style) stylize(style, 0, @plain.length) end |
#to_s ⇒ String
390 391 392 |
# File 'lib/rich/text.rb', line 390 def to_s @plain end |
#to_segments ⇒ Array<Segment>
Convert to segments for rendering
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/rich/text.rb', line 336 def to_segments return [Segment.new(@plain, style: @style)] if @spans.empty? # Build a list of style changes changes = [] @spans.each do |span| changes << [span.start, :start, span.style] changes << [span.end, :end, span.style] end changes.sort_by! { |c| [c[0], c[1] == :end ? 0 : 1] } segments = [] active_styles = [] pos = 0 changes.each do |change_pos, change_type, style| if change_pos > pos # Emit segment for text between pos and change_pos combined_style = combine_styles(active_styles) combined_style = @style + combined_style if @style && combined_style combined_style ||= @style segments << Segment.new(@plain[pos...change_pos], style: combined_style) end if change_type == :start active_styles << style else active_styles.delete_at(active_styles.rindex(style) || active_styles.length) end pos = change_pos end # Emit remaining text if pos < @plain.length combined_style = combine_styles(active_styles) combined_style = @style + combined_style if @style && combined_style combined_style ||= @style segments << Segment.new(@plain[pos..], style: combined_style) end segments end |
#wrap(width) ⇒ Array<Text>
Wrap text to a given width
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/rich/text.rb', line 228 def wrap(width) return [self.dup] if width <= 0 || cell_length <= width lines = [] current_line = Text.new(style: @style) current_width = 0 words = @plain.split(/(\s+)/) word_pos = 0 words.each do |word| word_width = Cells.cell_len(word) word_end = word_pos + word.length if current_width + word_width <= width # Word fits on the current line line_offset = current_line.length current_line.append(word) copy_spans_into(current_line, word_pos, word.length, line_offset) current_width += word_width elsif word_width > width # Word is longer than the whole width: hard-break it character by # character, carrying styles for every emitted character. unless current_line.empty? lines << current_line current_line = Text.new(style: @style) current_width = 0 end char_pos = word_pos word.each_char do |char| char_width = Cells.char_width(char) if current_width + char_width > width && !current_line.empty? lines << current_line current_line = Text.new(style: @style) current_width = 0 end line_offset = current_line.length current_line.append(char) copy_spans_into(current_line, char_pos, 1, line_offset) current_width += char_width char_pos += 1 end else # Word doesn't fit here but fits on a fresh line. Leading whitespace # is stripped at the start of the new line; account for it so spans # still line up. lines << current_line unless current_line.empty? current_line = Text.new(style: @style) stripped = word.lstrip lead = word.length - stripped.length line_offset = current_line.length current_line.append(stripped) copy_spans_into(current_line, word_pos + lead, stripped.length, line_offset) current_width = Cells.cell_len(stripped) end word_pos = word_end end lines << current_line unless current_line.empty? lines end |