@@ -6,11 +6,13 @@ module Strings
66
77 module_function
88
9+ ANSI_CODE = /\e \[ [0-9;]*m/
10+
911 # does this string contain ansi codes?
1012 def painted? ( str ) = str . match? ( /\e / )
1113
1214 # strip ansi codes
13- def unpaint ( str ) = str . gsub ( / \e \[ [0-9;]*m/ , "" )
15+ def unpaint ( str ) = str . gsub ( ANSI_CODE , "" )
1416
1517 # similar to rails titleize
1618 def titleize ( str )
@@ -49,50 +51,50 @@ def hyperlink(str)
4951 end
5052 end
5153
52- # truncate a string based on the display width of the grapheme clusters.
53- # Should handle emojis and international characters. Painted strings too.
54+ ELLIPSIS = "…"
55+ INVISIBLE = /\A (\e \[ [0-9;]*m|\u200B )*\z /
56+
57+ # Truncate a string based on the display width of characters. Does not
58+ # attempt to handle graphemes. Should handle emojis and international
59+ # characters. Painted strings too.
5460 def truncate ( str , stop )
55- if simple? ( str )
56- ( str . length > stop ) ? "#{ str [ 0 , stop - 1 ] } …" : str
57- elsif painted? ( str )
58- # generate truncated plain version
59- plain = truncate0 ( unpaint ( str ) , stop )
60- # make a best effort to apply the colors
61- if ( opening_codes = str [ /\e \[ (?:[0-9];?)+m/ ] )
62- "#{ opening_codes } #{ plain } #{ Paint ::NOTHING } "
63- else
64- plain
65- end
61+ if str . bytesize <= stop
62+ str
63+ elsif simple? ( str )
64+ ( str . length <= stop ) ? str : "#{ str [ 0 , stop - 1 ] } #{ ELLIPSIS } "
6665 else
6766 truncate0 ( str , stop )
6867 end
6968 end
7069
71- # slow, but handles graphemes
70+ # This is a slower truncate to handle ansi colors and wide characters like
71+ # emojis. Inspired by piotrmurach/strings-truncation
7272 def truncate0 ( text , stop )
73- # get grapheme clusters, and attach zero width graphemes to the previous grapheme
74- list = [ ] . tap do |accum |
75- text . grapheme_clusters . each do
76- if width ( _1 ) == 0 && !accum . empty?
77- accum [ -1 ] = "#{ accum [ -1 ] } #{ _1 } "
78- else
79- accum << _1
73+ [ ] . tap do |buf |
74+ scan , len , painting = StringScanner . new ( text ) , 0 , false
75+ until scan . eos?
76+ # are we looking at an ansi code?
77+ if scan . scan ( ANSI_CODE )
78+ buf << scan . matched
79+ painting = scan . matched != Paint ::NOTHING
80+ next
8081 end
81- end
82- end
8382
84- width = 0
85- list . each_index do
86- w = Unicode ::DisplayWidth . of ( list [ _1 ] )
87- next if ( width += w ) <= stop
83+ # what's next?
84+ ch = scan . getch
8885
89- # we've gone too far. do we need to pop for the ellipsis?
90- text = list [ 0 , _1 ]
91- text . pop if width - w == stop
92- return "#{ text . join } …"
93- end
86+ # done? append one final char, possible an ELLIPSIS
87+ len += Unicode ::DisplayWidth . of ( ch )
88+ if len >= stop
89+ buf << ( scan . check ( INVISIBLE ) ? ch : ELLIPSIS )
90+ break
91+ end
9492
95- text
93+ # keep going
94+ buf << ch
95+ end
96+ buf << Paint ::NOTHING if painting
97+ end . join
9698 end
9799 private_class_method :truncate0
98100
0 commit comments