Class: Tuile::StyledString
- Inherits:
-
Object
- Object
- Tuile::StyledString
- Defined in:
- lib/tuile/styled_string.rb,
sig/tuile.rbs
Overview
An immutable string-with-styling, modeled as a sequence of Spans where
each span carries a complete Style (fg, bg, bold, italic,
underline, strikethrough, inverse). Spans are non-overlapping and fully tile
the string — every character has exactly one resolved style, no overlay
layers to merge, so the style at any column is just its span's style
rather than a replay of the SGR state machine. The book's chapter 9 is
the long-form why (spans vs. a String full of escape codes).
Constructors
StyledString.new # empty
StyledString.plain("hello") # default style
StyledString.styled("hello", fg: :red, bold: true)
StyledString.parse("\e[31mhello\e[0m world") # ANSI → spans
Algebra
All operations return a fresh StyledString — the underlying spans are
frozen and shared. + coerces a String operand via StyledString.parse.
a + b # concatenate
ss.slice(2, 5) # 5 display columns starting at column 2
ss.slice(2..5) # range (inclusive end)
ss.lines # split on "\n" → Array<StyledString>
ss.each_char_with_style { |ch, style| ... }
Parser
StyledString.parse is strict by default — it recognizes only the SGR codes for
Style's attributes (fg/bg/bold/italic/underline/strikethrough/inverse) and
raises ParseError on anything else, keeping the parse(to_ansi(x)) == x
round-trip honest. Pass lenient: true to instead discard everything it
can't model (unmodeled SGR, cursor moves, OSC/DCS, stray escapes) and keep
only the recognized colors — lossy by design, for piping in colored output
you don't control. See the book for the full rationale.
Defined Under Namespace
Classes: ParseError, Span, Style
Constant Summary collapse
- EMOJI_WIDTH =
The framework's single emoji-width policy, passed to every
Unicode::DisplayWidth.ofcall in Tuile.:rgicredits width 2 only to RGI sequences — the ones vendors actually ship a single glyph for — and sums the parts of anything else. That is the one setting never wrong in the dangerous direction: under-measuring lets a glyph overrun its cell, which shifts the rest of the row, desyncs the cursor and escapes the component's rect, while over-measuring leaves a blank column.D_cluster_widthhas the per-setting reasoning. :rgi- EMPTY =
Canonical shared empty Tuile::StyledString. Operations that produce an empty result (and callers that need a blank sentinel) can use this instead of allocating a fresh instance per call. Pre-warmed and frozen — the lazy #display_width / #to_ansi memoizations short-circuit on the already cached values, so reads on the frozen receiver do not attempt writes.
new.tap do |s| s.display_width s.to_ansi end.freeze
Instance Attribute Summary collapse
-
#spans ⇒ ::Array[Span]
readonly
@return — the frozen, normalized span list — no empty-text entries, no two adjacent entries sharing a style.
Class Method Summary collapse
-
.parse(input, lenient: false) ⇒ StyledString
Parses an ANSI/SGR-coded string into a StyledString.
-
.plain(text) ⇒ StyledString
sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS @param
text. -
.styled(text, **style_kwargs) ⇒ StyledString
sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS @param
text.
Instance Method Summary collapse
-
#+(other) ⇒ StyledString
Concatenation.
-
#==(other) ⇒ Boolean
(also: #eql?)
@param
other. - #build_ansi ⇒ String
-
#display_width ⇒ Integer
Total display width in terminal columns, accounting for Unicode wide characters (fullwidth CJK = 2 columns, combining marks = 0, etc.).
-
#each_char_with_style ⇒ ::Enumerator[untyped], self
Yields each character (per
String#each_char) along with the Style it carries. -
#each_glyph_with_style(styled) ⇒ void
Like #each_char_with_style but per grapheme cluster.
-
#ellipsize(display_width, ellipsis = "…") ⇒ StyledString
Truncates to a target column width, appending an ellipsis when characters were dropped.
- #empty? ⇒ Boolean
-
#glyphs_to_styled(glyphs) ⇒ StyledString
@param
glyphs—[grapheme cluster, style, width]triples. -
#hard_break_glyphs(glyphs, width) ⇒ ::Array[::Array[::Array[untyped]]]
@param
glyphs—[grapheme cluster, style, width]triples. - #hash ⇒ Integer
-
#initialize(spans = []) ⇒ StyledString
constructor
@param
spans. - #inspect ⇒ String
-
#lines ⇒ ::Array[StyledString]
Splits on
"\n", preserving spans on each side. -
#normalize(spans) ⇒ ::Array[Span]
@param
spans. -
#resolve_slice_bounds(start_or_range, len, total) ⇒ [Integer, Integer]
@param
start_or_range. -
#slice(start_or_range, len = nil) ⇒ StyledString
Substring by display columns, preserving spans.
-
#slice_spans(start, len) ⇒ StyledString
@param
start. -
#slice_text_by_columns(text, start_col, len_col) ⇒ String
Walks grapheme clusters, so a slice boundary can never fall inside one: cutting a cluster would strand a combining mark with no base, which the painter drops outright, silently losing the accent off a letter.
-
#to_ansi ⇒ String
Rendered ANSI string.
-
#to_s ⇒ String
Plain text concatenation across all spans — no SGR codes.
-
#tokenize_for_wrap(line) ⇒ ::Array[::Array[untyped]]
Splits into whitespace/word tokens by grapheme cluster, not character: a cluster is the unit a terminal draws, so measuring its parts separately would both mis-total an emoji sequence and let a wrap break a letter away from its combining mark.
-
#under_bg(bg) ⇒ StyledString
Returns a copy with
bgset only on spans that have none; a span with an explicit bg is left untouched. -
#with_bg(bg) ⇒ StyledString
Returns a new StyledString with
bgapplied to every span, preserving each span's text and other style attributes (fg,bold,italic,underline,strikethrough). -
#with_bold(bold: true) ⇒ StyledString
Returns a new StyledString with
boldapplied to every span, preserving each span's text and other style attributes (fg,bg,italic,underline,strikethrough). -
#with_fg(fg) ⇒ StyledString
Returns a new StyledString with
fgapplied to every span, preserving each span's text and other style attributes (bg,bold,italic,underline,strikethrough). -
#with_inverse(inverse: true) ⇒ StyledString
Returns a new StyledString with
inverseapplied to every span, preserving each span's text and other style attributes. -
#with_underline(underline: true) ⇒ StyledString
Returns a new StyledString with
underlineapplied to every span, preserving each span's text and other style attributes (fg,bg,bold,italic,strikethrough). -
#wrap(width) ⇒ ::Array[StyledString]
Word-wraps to rows that each fit within
widthdisplay columns, preserving spans and styles across breaks. -
#wrap_one(line, width) ⇒ ::Array[StyledString]
@param
line— one line — no embedded"\n".
Constructor Details
#initialize(spans = []) ⇒ StyledString
@param spans
426 427 428 |
# File 'lib/tuile/styled_string.rb', line 426 def initialize(spans = []) @spans = normalize(spans).freeze end |
Instance Attribute Details
#spans ⇒ ::Array[Span] (readonly)
@return — the frozen, normalized span list — no empty-text entries, no two adjacent entries sharing a style.
423 424 425 |
# File 'lib/tuile/styled_string.rb', line 423 def spans @spans end |
Class Method Details
.parse(input, lenient: false) ⇒ StyledString
Parses an ANSI/SGR-coded string into a Tuile::StyledString. A Tuile::StyledString
input is returned as-is. nil and the empty string both fast-path to
EMPTY. Strings without any \e byte fast-path to a single
default-styled span.
@param input
@param lenient — when true, unmodeled SGR codes and non-SGR escapes are discarded instead of raising — see Tuile::StyledString "## Parser". Lossy: the result no longer round-trips to input.
393 394 395 396 397 398 399 400 401 402 403 404 405 |
# File 'lib/tuile/styled_string.rb', line 393 def parse(input, lenient: false) case input when nil then EMPTY when StyledString then input when String return EMPTY if input.empty? return new([Span.new(text: input, style: Style::DEFAULT)]) unless input.include?("\e") Parser.new(input, lenient:).parse else raise TypeError, "cannot parse #{input.class}" end end |
.plain(text) ⇒ StyledString
sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS
@param text
363 364 365 366 367 368 |
# File 'lib/tuile/styled_string.rb', line 363 def plain(text) text = text.to_s return EMPTY if text.empty? new([Span.new(text: text, style: Style::DEFAULT)]) end |
.styled(text, **style_kwargs) ⇒ StyledString
sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS
@param text
@param style_kwargs — forwarded to Tuile::StyledString::Style.new.
373 374 375 376 377 378 |
# File 'lib/tuile/styled_string.rb', line 373 def styled(text, **style_kwargs) text = text.to_s return EMPTY if text.empty? new([Span.new(text: text, style: Style.new(**style_kwargs))]) end |
Instance Method Details
#+(other) ⇒ StyledString
Concatenation. A String operand is parsed via parse before joining
(so embedded ANSI escapes round-trip through spans).
@param other
474 475 476 477 478 479 |
# File 'lib/tuile/styled_string.rb', line 474 def +(other) other = self.class.parse(other) if other.is_a?(String) raise TypeError, "cannot concatenate #{other.class} to StyledString" unless other.is_a?(StyledString) self.class.new(@spans + other.spans) end |
#==(other) ⇒ Boolean Also known as: eql?
@param other
459 460 461 |
# File 'lib/tuile/styled_string.rb', line 459 def ==(other) other.is_a?(StyledString) && @spans == other.spans end |
#build_ansi ⇒ String
714 715 716 717 718 719 720 721 722 723 724 |
# File 'lib/tuile/styled_string.rb', line 714 def build_ansi out = +"" current = Style::DEFAULT @spans.each do |span| out << current.sgr_to(span.style) out << span.text current = span.style end out << Ansi::RESET unless current.default? out end |
#display_width ⇒ Integer
Total display width in terminal columns, accounting for Unicode wide characters (fullwidth CJK = 2 columns, combining marks = 0, etc.).
433 434 435 |
# File 'lib/tuile/styled_string.rb', line 433 def display_width @display_width ||= @spans.sum { |s| Unicode::DisplayWidth.of(s.text, emoji: EMOJI_WIDTH) } end |
#each_char_with_style ⇒ ::Enumerator[untyped], self
Yields each character (per String#each_char) along with the Style
it carries. Returns an Enumerator without a block.
588 589 590 591 592 593 594 595 |
# File 'lib/tuile/styled_string.rb', line 588 def each_char_with_style return enum_for(__method__) unless block_given? @spans.each do |span| span.text.each_char { |c| yield c, span.style } end self end |
#each_glyph_with_style(styled) ⇒ void
This method returns an undefined value.
Like #each_char_with_style but per grapheme cluster. A cluster spanning a style boundary takes the style of its first span — pathological input, and splitting the cluster to honor both styles would paint a headless mark.
@param styled
873 874 875 876 877 |
# File 'lib/tuile/styled_string.rb', line 873 def each_glyph_with_style(styled) styled.spans.each do |span| span.text.each_grapheme_cluster { |g| yield g, span.style } end end |
#ellipsize(display_width, ellipsis = "…") ⇒ StyledString
Truncates to a target column width, appending an ellipsis when
characters were dropped. The ellipsis counts toward the target — the
returned Tuile::StyledString's display_width never exceeds
display_width. When self already fits, self is returned. When
display_width is smaller than the ellipsis's own width, the ellipsis
is sliced down to fit and no original content is included.
@param display_width — target column width.
@param ellipsis — appended when truncation occurs. Defaults to the Unicode horizontal-ellipsis … (one column). A String is parsed via parse, so ANSI in it is preserved.
516 517 518 519 520 521 522 523 524 |
# File 'lib/tuile/styled_string.rb', line 516 def ellipsize(display_width, ellipsis = "…") return self.class.new if display_width <= 0 return self if self.display_width <= display_width ellipsis = self.class.parse(ellipsis) return ellipsis.slice(0, display_width) if ellipsis.display_width >= display_width slice(0, display_width - ellipsis.display_width) + ellipsis end |
#empty? ⇒ Boolean
438 |
# File 'lib/tuile/styled_string.rb', line 438 def empty? = @spans.empty? |
#glyphs_to_styled(glyphs) ⇒ StyledString
@param glyphs — [grapheme cluster, style, width] triples.
902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 |
# File 'lib/tuile/styled_string.rb', line 902 def glyphs_to_styled(glyphs) return self.class.new if glyphs.empty? spans = [] current_text = +"" current_style = glyphs.first[1] glyphs.each do |g, s, _| if s == current_style current_text << g else spans << Span.new(text: current_text, style: current_style) current_text = +g current_style = s end end spans << Span.new(text: current_text, style: current_style) self.class.new(spans) end |
#hard_break_glyphs(glyphs, width) ⇒ ::Array[::Array[::Array[untyped]]]
@param glyphs — [grapheme cluster, style, width] triples.
@param width
@return — each inner Array is a glyphs-shaped chunk.
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 |
# File 'lib/tuile/styled_string.rb', line 882 def hard_break_glyphs(glyphs, width) chunks = [] current = [] current_w = 0 glyphs.each do |triple| cw = triple[2] if current_w + cw > width && current_w.positive? chunks << current current = [] current_w = 0 end current << triple current_w += cw end chunks << current chunks end |
#hash ⇒ Integer
465 466 467 |
# File 'lib/tuile/styled_string.rb', line 465 def hash @spans.hash end |
#inspect ⇒ String
707 708 709 |
# File 'lib/tuile/styled_string.rb', line 707 def inspect "#<#{self.class.name} #{to_s.inspect}>" end |
#lines ⇒ ::Array[StyledString]
Splits on "\n", preserving spans on each side. A trailing newline
produces a trailing empty Tuile::StyledString (matches split("\n", -1)).
An empty Tuile::StyledString returns a single empty entry, like "".split.
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 |
# File 'lib/tuile/styled_string.rb', line 530 def lines result = [] current_spans = [] @spans.each do |span| parts = span.text.split("\n", -1) parts.each_with_index do |part, idx| if idx.positive? result << self.class.new(current_spans) current_spans = [] end current_spans << Span.new(text: part, style: span.style) unless part.empty? end end result << self.class.new(current_spans) result end |
#normalize(spans) ⇒ ::Array[Span]
@param spans
728 729 730 731 732 733 734 735 736 737 738 739 740 741 |
# File 'lib/tuile/styled_string.rb', line 728 def normalize(spans) result = [] spans.each do |span| next if span.text.empty? if !result.empty? && result.last.style == span.style last = result.pop result << Span.new(text: last.text + span.text, style: span.style) else result << span end end result end |
#resolve_slice_bounds(start_or_range, len, total) ⇒ [Integer, Integer]
@param start_or_range
@param len
@param total — receiver's full display width.
@return — normalized [start_col, len_col].
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 |
# File 'lib/tuile/styled_string.rb', line 747 def resolve_slice_bounds(start_or_range, len, total) if start_or_range.is_a?(Range) range = start_or_range start = range.begin || 0 finish = range.end start += total if start.negative? if finish.nil? finish = total else finish += total if finish.negative? finish += 1 unless range.exclude_end? end [start, finish - start] else raise ArgumentError, "length is required when slicing with an Integer" if len.nil? start = start_or_range start += total if start.negative? [start, len] end end |
#slice(start_or_range, len = nil) ⇒ StyledString
Substring by display columns, preserving spans. Characters whose column range only partially overlaps the slice (e.g. a 2-column CJK character straddling the start or end boundary) are dropped — never split.
Accepts either slice(start_col, len_col) or slice(range). Both
forms support negative indices counting from the end of the string.
@param start_col
@param len_col
494 495 496 497 498 499 500 501 |
# File 'lib/tuile/styled_string.rb', line 494 def slice(start_or_range, len = nil) total = display_width start, len = resolve_slice_bounds(start_or_range, len, total) return self.class.new if len <= 0 || start.negative? || start >= total len = [len, total - start].min slice_spans(start, len) end |
#slice_spans(start, len) ⇒ StyledString
@param start
@param len
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 |
# File 'lib/tuile/styled_string.rb', line 772 def slice_spans(start, len) out = [] col = 0 @spans.each do |span| span_width = Unicode::DisplayWidth.of(span.text, emoji: EMOJI_WIDTH) span_end = col + span_width next col = span_end if span_end <= start break if col >= start + len local_start = [0, start - col].max local_end = [span_width, start + len - col].min if local_end > local_start sliced = slice_text_by_columns(span.text, local_start, local_end - local_start) out << Span.new(text: sliced, style: span.style) unless sliced.empty? end col = span_end end self.class.new(out) end |
#slice_text_by_columns(text, start_col, len_col) ⇒ String
Walks grapheme clusters, so a slice boundary can never fall inside one: cutting a cluster would strand a combining mark with no base, which the painter drops outright, silently losing the accent off a letter.
@param text
@param start_col
@param len_col
928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 |
# File 'lib/tuile/styled_string.rb', line 928 def slice_text_by_columns(text, start_col, len_col) out = +"" col = 0 text.each_grapheme_cluster do |g| gw = Buffer.display_width(g) glyph_end = col + gw if glyph_end <= start_col # entirely before slice — skip elsif col >= start_col + len_col break elsif col >= start_col && glyph_end <= start_col + len_col out << g end # any other case = partial overlap with a wide glyph — drop col = glyph_end end out end |
#to_ansi ⇒ String
Rendered ANSI string. Minimal-diff between adjacent spans: only the
attributes that changed are emitted. A transition to the default style
emits \e[0m (one code) instead of the longer "turn each attribute
off" form. Always closes with \e[0m when the last span carried a
non-default style, so the styled run doesn't bleed into subsequent
output.
453 454 455 |
# File 'lib/tuile/styled_string.rb', line 453 def to_ansi @to_ansi ||= build_ansi end |
#to_s ⇒ String
Plain text concatenation across all spans — no SGR codes.
442 443 444 |
# File 'lib/tuile/styled_string.rb', line 442 def to_s @spans.map(&:text).join end |
#tokenize_for_wrap(line) ⇒ ::Array[::Array[untyped]]
Splits into whitespace/word tokens by grapheme cluster, not character: a cluster is the unit a terminal draws, so measuring its parts separately would both mis-total an emoji sequence and let a wrap break a letter away from its combining mark.
@param line
@return — tokens shaped [type, glyphs, w] where type is
:space or :word, glyphs is an Array<[String, Style, Integer]>
(grapheme cluster, style, display width), and w is the token's total
width.
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 |
# File 'lib/tuile/styled_string.rb', line 844 def tokenize_for_wrap(line) tokens = [] current_glyphs = [] current_w = 0 current_type = nil each_glyph_with_style(line) do |g, s| type = [" ", "\t"].include?(g) ? :space : :word gw = Buffer.display_width(g) if current_type && current_type != type tokens << [current_type, current_glyphs, current_w] current_glyphs = [] current_w = 0 end current_type = type current_glyphs << [g, s, gw] current_w += gw end tokens << [current_type, current_glyphs, current_w] unless current_glyphs.empty? tokens end |
#under_bg(bg) ⇒ StyledString
Returns a copy with bg set only on spans that have none; a span with
an explicit bg is left untouched. The fill-unset counterpart of #with_bg
(which overrides every span) — it slides a background under the content,
so a log line keeps its red error-level bg while its plain text picks up an
inherited panel tint.
An inverse span counts as backgrounded and is skipped even when its bg
member is nil: SGR 7 swaps the pair in effect, so a bg filled under it
would recolor the span's glyphs, not the ground behind them.
@param bg — background color, coerced via Color.coerce. nil returns self unchanged.
623 624 625 626 627 628 629 630 631 632 633 634 |
# File 'lib/tuile/styled_string.rb', line 623 def under_bg(bg) return self if bg.nil? bg = Color.coerce(bg) self.class.new(@spans.map do |span| if span.style.bg.nil? && !span.style.inverse Span.new(text: span.text, style: span.style.merge(bg: bg)) else span end end) end |
#with_bg(bg) ⇒ StyledString
Returns a new Tuile::StyledString with bg applied to every span, preserving
each span's text and other style attributes (fg, bold, italic,
underline, strikethrough). Useful for row-level highlights — the new bg overlays
without dropping foreground colors the original styling carried.
@param bg — background color, coerced via Color.coerce. nil clears bg back to the terminal default.
606 607 608 |
# File 'lib/tuile/styled_string.rb', line 606 def with_bg(bg) self.class.new(@spans.map { |span| Span.new(text: span.text, style: span.style.merge(bg: bg)) }) end |
#with_bold(bold: true) ⇒ StyledString
Returns a new Tuile::StyledString with bold applied to every span, preserving
each span's text and other style attributes (fg, bg, italic,
underline, strikethrough). The bold-attribute counterpart of
#with_bg / #with_fg: it emphasizes a whole run of app-authored,
possibly multi-span content — a widget marking one caption out of several
as selected, where the caption may already carry its own colors.
There is deliberately no under_bold (the fill-unset counterpart
#under_bg provides for backgrounds): a background is inherited down the
component tree, so a span with none has a meaningful "unset" state to
fill, while bold is a plain per-span attribute that is either on or
off. Pass bold: false to clear it.
@param bold — whether the spans should be bold.
664 665 666 |
# File 'lib/tuile/styled_string.rb', line 664 def with_bold(bold: true) self.class.new(@spans.map { |span| Span.new(text: span.text, style: span.style.merge(bold:)) }) end |
#with_fg(fg) ⇒ StyledString
Returns a new Tuile::StyledString with fg applied to every span, preserving
each span's text and other style attributes (bg, bold, italic,
underline, strikethrough). The new fg overlays without dropping background colors or
text attributes the original styling carried.
@param fg — foreground color, coerced via Color.coerce. nil clears fg back to the terminal default.
645 646 647 |
# File 'lib/tuile/styled_string.rb', line 645 def with_fg(fg) self.class.new(@spans.map { |span| Span.new(text: span.text, style: span.style.merge(fg: fg)) }) end |
#with_inverse(inverse: true) ⇒ StyledString
Returns a new Tuile::StyledString with inverse applied to every span,
preserving each span's text and other style attributes. Inverse swaps
whatever fg/bg are actually in effect at each cell — terminal defaults
included — so a focus chip built with it reads as "backgrounded" on any
terminal palette without picking a single color:
StyledString.plain(" 1 VMs ").with_inverse # the inverted-chip idiom
There is deliberately no under_inverse, for the reason #with_bold
spells out. Pass inverse: false to clear it.
@param inverse — whether the spans should be inverted.
702 703 704 |
# File 'lib/tuile/styled_string.rb', line 702 def with_inverse(inverse: true) self.class.new(@spans.map { |span| Span.new(text: span.text, style: span.style.merge(inverse:)) }) end |
#with_underline(underline: true) ⇒ StyledString
Returns a new Tuile::StyledString with underline applied to every span,
preserving each span's text and other style attributes (fg, bg,
bold, italic, strikethrough). Slice and rejoin to underline part
of a string, which is what a one-character cue needs:
cap = StyledString.parse("File")
cap.slice(0, 1).with_underline + cap.slice(1, cap.display_width - 1)
# => "File" with the F underlined — a menu mnemonic
Note #slice counts columns, not characters, so a caption with a wide glyph before the cue needs the prefix measured rather than counted.
There is deliberately no under_underline, for the reason #with_bold
spells out. Pass underline: false to clear it.
@param underline — whether the spans should be underlined.
685 686 687 |
# File 'lib/tuile/styled_string.rb', line 685 def with_underline(underline: true) self.class.new(@spans.map { |span| Span.new(text: span.text, style: span.style.merge(underline:)) }) end |
#wrap(width) ⇒ ::Array[StyledString]
Word-wraps to rows that each fit within width display
columns, preserving spans and styles across breaks. Greedy word-wrap,
hard-break for words wider than width, leading whitespace dropped on
wrapped continuations, hard "\n" breaks preserved as separate output
lines.
An indent is content, so it survives onto the first row — but there is no hanging indent:
StyledString.plain(" read config").wrap(20).map(&:to_s)
# => [" read config"] indent kept; the line never wrapped
StyledString.plain(" read config").wrap(6).map(&:to_s)
# => [" read", "config"] ...but a continuation starts at column 0
Whitespace runs are space or tab; other characters are treated as word
content. When a single character is wider than width (e.g. a 2-column
CJK character with width = 1), it is still emitted on its own line at
its natural width. The "no line exceeds width" guarantee therefore
holds whenever every character is at most width columns wide. An indent
that alone exceeds width is dropped rather than given a row of its own.
@param width — target column width. nil or <= 0 skips wrapping and returns each hard-line as-is, so callers can pass a stale viewport width without crashing.
@return — one entry per output row.
An empty receiver returns [].
573 574 575 576 577 578 579 580 581 582 |
# File 'lib/tuile/styled_string.rb', line 573 def wrap(width) return [] if empty? input_lines = lines return input_lines if width.nil? || width <= 0 result = [] input_lines.each { |line| result.concat(wrap_one(line, width)) } result end |
#wrap_one(line, width) ⇒ ::Array[StyledString]
@param line — one line — no embedded "\n".
@param width
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 |
# File 'lib/tuile/styled_string.rb', line 796 def wrap_one(line, width) return [line] if line.empty? result = [] line_glyphs = [] line_w = 0 tokenize_for_wrap(line).each do |type, glyphs, w| if type == :space if line_w.zero? && (!result.empty? || w > width) # Nothing to emit: a continuation's leading run was consumed by the # break, and an indent wider than the viewport conveys no nesting. elsif line_w + w <= width line_glyphs.concat(glyphs) line_w += w else result << glyphs_to_styled(line_glyphs) line_glyphs = [] line_w = 0 end elsif line_w + w <= width line_glyphs.concat(glyphs) line_w += w elsif w > width result << glyphs_to_styled(line_glyphs) unless line_w.zero? chunks = hard_break_glyphs(glyphs, width) chunks[0..-2].each { |chunk| result << glyphs_to_styled(chunk) } line_glyphs = chunks.last line_w = line_glyphs.sum { |triple| triple[2] } else result << glyphs_to_styled(line_glyphs) line_glyphs = glyphs line_w = w end end result << glyphs_to_styled(line_glyphs) result end |