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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Strings
2
- VERSION = "0.0.0"
2
+ VERSION = '0.1.0'.freeze
3
3
  end
@@ -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
@@ -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 module to wrap, truncate, indent, and otherwise transform strings.}
13
- spec.description = %q{A module to wrap, truncate, indent, and otherwise transofrm strings.}
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"