strings 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.travis.yml +20 -3
- data/CHANGELOG.md +7 -0
- data/Gemfile +10 -1
- data/README.md +340 -5
- data/Rakefile +5 -3
- data/appveyor.yml +21 -0
- data/assets/strings_logo.png +0 -0
- data/benchmarks/speed_profile.rb +30 -0
- data/lib/strings.rb +109 -3
- data/lib/strings/align.rb +141 -0
- data/lib/strings/ansi.rb +68 -0
- data/lib/strings/fold.rb +27 -0
- data/lib/strings/pad.rb +94 -0
- data/lib/strings/padder.rb +160 -0
- data/lib/strings/truncate.rb +107 -0
- data/lib/strings/version.rb +1 -1
- data/lib/strings/wrap.rb +165 -0
- data/strings.gemspec +5 -2
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- metadata +47 -4
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_litera: true
|
2
|
+
|
3
|
+
module Strings
|
4
|
+
# A class responsible for parsing padding value
|
5
|
+
#
|
6
|
+
# Used internally by {Strings::Pad}
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Padder
|
10
|
+
ParseError = Class.new(ArgumentError)
|
11
|
+
|
12
|
+
# Parse padding options
|
13
|
+
#
|
14
|
+
# Turn possible values into 4 element array
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# padder = TTY::Table::Padder.parse(5)
|
18
|
+
# padder.padding # => [5, 5, 5, 5]
|
19
|
+
#
|
20
|
+
# @param [Object] value
|
21
|
+
#
|
22
|
+
# @return [TTY::Padder]
|
23
|
+
# the new padder with padding values
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def self.parse(value = nil)
|
27
|
+
return value if value.is_a?(self)
|
28
|
+
|
29
|
+
new(convert_to_ary(value))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convert value to 4 element array
|
33
|
+
#
|
34
|
+
# @return [Array[Integer]]
|
35
|
+
# the 4 element padding array
|
36
|
+
#
|
37
|
+
# @api private
|
38
|
+
def self.convert_to_ary(value)
|
39
|
+
if value.class <= Numeric
|
40
|
+
[value, value, value, value]
|
41
|
+
elsif value.nil?
|
42
|
+
[]
|
43
|
+
elsif value.size == 2
|
44
|
+
[value[0], value[1], value[0], value[1]]
|
45
|
+
elsif value.size == 4
|
46
|
+
value
|
47
|
+
else
|
48
|
+
raise ParseError, 'Wrong :padding parameter, must be an array'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Padding
|
53
|
+
#
|
54
|
+
# @return [Array[Integer]]
|
55
|
+
attr_reader :padding
|
56
|
+
|
57
|
+
# Initialize a Padder
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def initialize(padding)
|
61
|
+
@padding = padding
|
62
|
+
end
|
63
|
+
|
64
|
+
# Top padding
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
def top
|
70
|
+
@padding[0].to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
# Set top padding
|
74
|
+
#
|
75
|
+
# @param [Integer] val
|
76
|
+
#
|
77
|
+
# @return [nil]
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def top=(value)
|
81
|
+
@padding[0] = value
|
82
|
+
end
|
83
|
+
|
84
|
+
# Right padding
|
85
|
+
#
|
86
|
+
# @return [Integer]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def right
|
90
|
+
@padding[1].to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
# Set right padding
|
94
|
+
#
|
95
|
+
# @param [Integer] val
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
def right=(value)
|
99
|
+
@padding[1] = value
|
100
|
+
end
|
101
|
+
|
102
|
+
# Bottom padding
|
103
|
+
#
|
104
|
+
# @return [Integer]
|
105
|
+
#
|
106
|
+
# @api public
|
107
|
+
def bottom
|
108
|
+
@padding[2].to_i
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set bottom padding
|
112
|
+
#
|
113
|
+
# @param [Integer] value
|
114
|
+
#
|
115
|
+
# @return [nil]
|
116
|
+
#
|
117
|
+
# @api public
|
118
|
+
def bottom=(value)
|
119
|
+
@padding[2] = value
|
120
|
+
end
|
121
|
+
|
122
|
+
# Left padding
|
123
|
+
#
|
124
|
+
# @return [Integer]
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
def left
|
128
|
+
@padding[3].to_i
|
129
|
+
end
|
130
|
+
|
131
|
+
# Set left padding
|
132
|
+
#
|
133
|
+
# @param [Integer] value
|
134
|
+
#
|
135
|
+
# @return [nil]
|
136
|
+
#
|
137
|
+
# @api public
|
138
|
+
def left=(value)
|
139
|
+
@padding[3] = value
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if padding is set
|
143
|
+
#
|
144
|
+
# @return [Boolean]
|
145
|
+
#
|
146
|
+
# @api public
|
147
|
+
def empty?
|
148
|
+
padding.empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
# String represenation of this padder with padding values
|
152
|
+
#
|
153
|
+
# @return [String]
|
154
|
+
#
|
155
|
+
# @api public
|
156
|
+
def to_s
|
157
|
+
inspect
|
158
|
+
end
|
159
|
+
end # Padder
|
160
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'unicode/display_width'
|
4
|
+
require 'unicode_utils/each_grapheme'
|
5
|
+
|
6
|
+
require_relative 'ansi'
|
7
|
+
|
8
|
+
module Strings
|
9
|
+
# A module responsible for text truncation
|
10
|
+
module Truncate
|
11
|
+
DEFAULT_TRAILING = '…'.freeze
|
12
|
+
|
13
|
+
DEFAULT_LENGTH = 30
|
14
|
+
|
15
|
+
# Truncate a text at a given length (defualts to 30)
|
16
|
+
#
|
17
|
+
# @param [String] text
|
18
|
+
# the text to be truncated
|
19
|
+
#
|
20
|
+
# @param [Integer] truncate_at
|
21
|
+
# the width at which to truncate the text
|
22
|
+
#
|
23
|
+
# @param [Hash] options
|
24
|
+
# @option options [Symbol] :separator the character for splitting words
|
25
|
+
# @option options [Symbol] :trailing the character for ending sentence
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# text = "The sovereignest thing on earth is parmacetti for an inward bruise."
|
29
|
+
#
|
30
|
+
# Strings::Truncate.truncate(text)
|
31
|
+
# # => "The sovereignest thing on ear…"
|
32
|
+
#
|
33
|
+
# Strings::Truncate.truncate(text, 20)
|
34
|
+
# # => "The sovereignest th…"
|
35
|
+
#
|
36
|
+
# Strings::Truncate.truncate(text, 20, separator: ' ' )
|
37
|
+
# # => "The sovereignest…"
|
38
|
+
#
|
39
|
+
# Strings::Truncate.truncate(40, trailing: '... (see more)' )
|
40
|
+
# # => "The sovereignest thing on... (see more)"
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def truncate(text, truncate_at = DEFAULT_LENGTH, options = {})
|
44
|
+
if truncate_at.is_a?(Hash)
|
45
|
+
options = truncate_at
|
46
|
+
truncate_at = DEFAULT_LENGTH
|
47
|
+
end
|
48
|
+
|
49
|
+
if display_width(text) <= truncate_at.to_i || truncate_at.to_i.zero?
|
50
|
+
return text.dup
|
51
|
+
end
|
52
|
+
|
53
|
+
trail = options.fetch(:trailing) { DEFAULT_TRAILING }
|
54
|
+
separation = options.fetch(:separator) { nil }
|
55
|
+
sanitized_text = Strings::ANSI.sanitize(text)
|
56
|
+
|
57
|
+
length_without_trailing = truncate_at - display_width(trail)
|
58
|
+
chars = to_chars(sanitized_text).to_a
|
59
|
+
stop = chars[0, length_without_trailing].rindex(separation)
|
60
|
+
slice_length = stop || length_without_trailing
|
61
|
+
sliced_chars = chars[0, slice_length]
|
62
|
+
original_chars = to_chars(text).to_a[0, 3 * slice_length]
|
63
|
+
shorten(original_chars, sliced_chars, length_without_trailing).join + trail
|
64
|
+
end
|
65
|
+
module_function :truncate
|
66
|
+
|
67
|
+
# Perform actual shortening of the text
|
68
|
+
#
|
69
|
+
# @return [String]
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
def shorten(original_chars, chars, length_without_trailing)
|
73
|
+
truncated = []
|
74
|
+
char_width = display_width(chars[0])
|
75
|
+
while length_without_trailing - char_width > 0
|
76
|
+
orig_char = original_chars.shift
|
77
|
+
char = chars.shift
|
78
|
+
break unless char
|
79
|
+
while orig_char != char # consume ansi
|
80
|
+
ansi = true
|
81
|
+
truncated << orig_char
|
82
|
+
orig_char = original_chars.shift
|
83
|
+
end
|
84
|
+
truncated << char
|
85
|
+
char_width = display_width(char)
|
86
|
+
length_without_trailing -= char_width
|
87
|
+
end
|
88
|
+
truncated << ["\e[0m"] if ansi
|
89
|
+
truncated
|
90
|
+
end
|
91
|
+
module_function :shorten
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
def to_chars(text)
|
95
|
+
UnicodeUtils.each_grapheme(text)
|
96
|
+
end
|
97
|
+
module_function :to_chars
|
98
|
+
|
99
|
+
# Visible width of a string
|
100
|
+
#
|
101
|
+
# @api private
|
102
|
+
def display_width(string)
|
103
|
+
Unicode::DisplayWidth.of(Strings::ANSI.sanitize(string))
|
104
|
+
end
|
105
|
+
module_function :display_width
|
106
|
+
end # Truncate
|
107
|
+
end # Strings
|
data/lib/strings/version.rb
CHANGED
data/lib/strings/wrap.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'unicode/display_width'
|
4
|
+
require 'unicode_utils/each_grapheme'
|
5
|
+
|
6
|
+
require_relative 'ansi'
|
7
|
+
require_relative 'fold'
|
8
|
+
|
9
|
+
module Strings
|
10
|
+
module Wrap
|
11
|
+
DEFAULT_WIDTH = 80
|
12
|
+
|
13
|
+
NEWLINE = "\n".freeze
|
14
|
+
|
15
|
+
SPACE = ' '.freeze
|
16
|
+
|
17
|
+
LINE_BREAK = "\r\n+|\r+|\n+".freeze
|
18
|
+
|
19
|
+
# Wrap a text into lines no longer than wrap_at length.
|
20
|
+
# Preserves existing lines and existing word boundaries.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# Strings::Wrap.wrap("Some longish text", 8)
|
24
|
+
# # => >Some
|
25
|
+
# >longish
|
26
|
+
# >text
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def wrap(text, wrap_at = DEFAULT_WIDTH)
|
30
|
+
if text.length < wrap_at.to_i || wrap_at.to_i.zero?
|
31
|
+
return text
|
32
|
+
end
|
33
|
+
ansi_stack = []
|
34
|
+
text.split(%r{#{LINE_BREAK}}, -1).map do |paragraph|
|
35
|
+
format_paragraph(paragraph, wrap_at, ansi_stack)
|
36
|
+
end * NEWLINE
|
37
|
+
end
|
38
|
+
module_function :wrap
|
39
|
+
|
40
|
+
# Format paragraph to be maximum of wrap_at length
|
41
|
+
#
|
42
|
+
# @param [String] paragraph
|
43
|
+
# the paragraph to format
|
44
|
+
# @param [Integer] wrap_at
|
45
|
+
# the maximum length to wrap the paragraph
|
46
|
+
#
|
47
|
+
# @return [Array[String]]
|
48
|
+
# the wrapped lines
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
def format_paragraph(paragraph, wrap_at, ansi_stack)
|
52
|
+
cleared_para = Fold.fold(paragraph)
|
53
|
+
lines = []
|
54
|
+
line = []
|
55
|
+
word = []
|
56
|
+
ansi = []
|
57
|
+
ansi_matched = false
|
58
|
+
word_length = 0
|
59
|
+
line_length = 0
|
60
|
+
char_length = 0 # visible char length
|
61
|
+
text_length = display_width(cleared_para)
|
62
|
+
total_length = 0
|
63
|
+
|
64
|
+
UnicodeUtils.each_grapheme(cleared_para) do |char|
|
65
|
+
# we found ansi let's consume
|
66
|
+
if char == Strings::ANSI::CSI || ansi.length > 0
|
67
|
+
ansi << char
|
68
|
+
if Strings::ANSI.only_ansi?(ansi.join)
|
69
|
+
ansi_matched = true
|
70
|
+
elsif ansi_matched
|
71
|
+
ansi_stack << [ansi[0...-1].join, line_length + word_length]
|
72
|
+
ansi_matched = false
|
73
|
+
ansi = []
|
74
|
+
end
|
75
|
+
next if ansi.length > 0
|
76
|
+
end
|
77
|
+
|
78
|
+
char_length = display_width(char)
|
79
|
+
total_length += char_length
|
80
|
+
if line_length + word_length + char_length <= wrap_at
|
81
|
+
if char == SPACE || total_length == text_length
|
82
|
+
line << word.join + char
|
83
|
+
line_length += word_length + char_length
|
84
|
+
word = []
|
85
|
+
word_length = 0
|
86
|
+
else
|
87
|
+
word << char
|
88
|
+
word_length += char_length
|
89
|
+
end
|
90
|
+
next
|
91
|
+
end
|
92
|
+
|
93
|
+
if char == SPACE # ends with space
|
94
|
+
lines << insert_ansi(ansi_stack, line.join)
|
95
|
+
line = []
|
96
|
+
line_length = 0
|
97
|
+
word << char
|
98
|
+
word_length += char_length
|
99
|
+
elsif word_length + char_length <= wrap_at
|
100
|
+
lines << insert_ansi(ansi_stack, line.join)
|
101
|
+
line = [word.join + char]
|
102
|
+
line_length = word_length + char_length
|
103
|
+
word = []
|
104
|
+
word_length = 0
|
105
|
+
else # hyphenate word - too long to fit a line
|
106
|
+
lines << insert_ansi(ansi_stack, word.join)
|
107
|
+
line_length = 0
|
108
|
+
word = [char]
|
109
|
+
word_length = char_length
|
110
|
+
end
|
111
|
+
end
|
112
|
+
lines << insert_ansi(ansi_stack, line.join) unless line.empty?
|
113
|
+
lines << insert_ansi(ansi_stack, word.join) unless word.empty?
|
114
|
+
lines
|
115
|
+
end
|
116
|
+
module_function :format_paragraph
|
117
|
+
|
118
|
+
# Insert ANSI code into string
|
119
|
+
#
|
120
|
+
# Check if there are any ANSI states, if present
|
121
|
+
# insert ANSI codes at given positions unwinding the stack.
|
122
|
+
#
|
123
|
+
# @param [Array[Array[String, Integer]]] ansi_stack
|
124
|
+
# the ANSI codes to apply
|
125
|
+
#
|
126
|
+
# @param [String] string
|
127
|
+
# the string to insert ANSI codes into
|
128
|
+
#
|
129
|
+
# @return [String]
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def insert_ansi(ansi_stack, string)
|
133
|
+
return string if ansi_stack.empty?
|
134
|
+
to_remove = 0
|
135
|
+
reset_index = -1
|
136
|
+
output = string.dup
|
137
|
+
resetting = false
|
138
|
+
ansi_stack.reverse_each do |state|
|
139
|
+
if state[0] =~ /#{Regexp.quote(Strings::ANSI::RESET)}/
|
140
|
+
resetting = true
|
141
|
+
reset_index = state[1]
|
142
|
+
to_remove += 2
|
143
|
+
next
|
144
|
+
elsif !resetting
|
145
|
+
reset_index = -1
|
146
|
+
resetting = false
|
147
|
+
end
|
148
|
+
|
149
|
+
color, color_index = *state
|
150
|
+
output.insert(reset_index, Strings::ANSI::RESET).insert(color_index, color)
|
151
|
+
end
|
152
|
+
ansi_stack.pop(to_remove) # remove used states
|
153
|
+
output
|
154
|
+
end
|
155
|
+
module_function :insert_ansi
|
156
|
+
|
157
|
+
# Visible width of a string
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
def display_width(string)
|
161
|
+
Unicode::DisplayWidth.of(Strings::ANSI.sanitize(string))
|
162
|
+
end
|
163
|
+
module_function :display_width
|
164
|
+
end # Wrap
|
165
|
+
end # Strings
|
data/strings.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Piotr Murach"]
|
10
10
|
spec.email = [""]
|
11
11
|
|
12
|
-
spec.summary = %q{A
|
13
|
-
spec.description = %q{A
|
12
|
+
spec.summary = %q{A set of useful functions for transforming strings.}
|
13
|
+
spec.description = %q{A set of useful functions such as fold, truncate, wrap and more for transoforming strings.}
|
14
14
|
spec.homepage = ""
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
@@ -21,6 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
+
spec.add_dependency 'unicode_utils', '~> 1.4.0'
|
25
|
+
spec.add_dependency 'unicode-display_width','~> 1.3.0'
|
26
|
+
|
24
27
|
spec.add_development_dependency "bundler", "~> 1.15"
|
25
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|