strings-truncation 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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +247 -0
- data/lib/strings-truncation.rb +1 -0
- data/lib/strings/truncation.rb +368 -0
- data/lib/strings/truncation/configuration.rb +119 -0
- data/lib/strings/truncation/extensions.rb +15 -0
- data/lib/strings/truncation/version.rb +7 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3837ca8b06eb6173b46ec33382369c9cb6f1f1a78515cf318658dfdf2f3dbce1
|
4
|
+
data.tar.gz: 49be33a7cc3f22afbf77c2f168704f46234b5312ada53a4aefcdb4d315143e14
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6308a3c890c3fd4f603399eb27cbe7faba13dbbc318497a3974d2db31ea3ed2827056162fb96eec522fc4dcbf2cd7b3e1eb84293500a041a8bcdbf349a554a9e
|
7
|
+
data.tar.gz: abf85e81c7fff5e9d0036c9e039a5880de95fcf9fdceef2d24abbeb908be8d6e07fdd09010d2e1a37e95e2b28395ccea7791d6c456c1772302e0fd62d5ebd9e0
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Piotr Murach
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,247 @@
|
|
1
|
+
<div align="center">
|
2
|
+
<img width="225" src="https://github.com/piotrmurach/strings/blob/master/assets/strings_logo.png" alt="strings logo" />
|
3
|
+
</div>
|
4
|
+
|
5
|
+
# Strings::Truncation
|
6
|
+
|
7
|
+
[][gem]
|
8
|
+
[][gh_actions_ci]
|
9
|
+
[][appveyor]
|
10
|
+
[][codeclimate]
|
11
|
+
[][coverage]
|
12
|
+
[][inchpages]
|
13
|
+
|
14
|
+
[gem]: http://badge.fury.io/rb/strings-truncation
|
15
|
+
[gh_actions_ci]: https://github.com/piotrmurach/strings-truncation/actions?query=workflow%3ACI
|
16
|
+
[appveyor]: https://ci.appveyor.com/project/piotrmurach/strings-truncation
|
17
|
+
[codeclimate]: https://codeclimate.com/github/piotrmurach/strings-truncation/maintainability
|
18
|
+
[coverage]: https://coveralls.io/github/piotrmurach/strings-truncation?branch=master
|
19
|
+
[inchpages]: http://inch-ci.org/github/piotrmurach/strings-truncation
|
20
|
+
|
21
|
+
> Truncate strings with fullwidth characters and ANSI codes.
|
22
|
+
|
23
|
+
## Features
|
24
|
+
|
25
|
+
* No monkey-patching String class
|
26
|
+
* Omit text from the start, middle, end or both ends
|
27
|
+
* Account for fullwidth characters in encodings such as UTF-8, EUC-JP
|
28
|
+
* Shorten text without whitespaces between words (Chinese, Japanese, Korean etc)
|
29
|
+
* Preserve ANSI escape codes
|
30
|
+
|
31
|
+
## Contents
|
32
|
+
|
33
|
+
* [1. Usage](#1-usage)
|
34
|
+
* [2. API](#2-api)
|
35
|
+
* [2.1 configure](#21-configure)
|
36
|
+
* [2.2 truncate](#22-truncate)
|
37
|
+
* [3. Extending String class](#3-extending-string-class)
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add this line to your application's Gemfile:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
gem "strings-truncation"
|
45
|
+
```
|
46
|
+
|
47
|
+
And then execute:
|
48
|
+
|
49
|
+
$ bundle
|
50
|
+
|
51
|
+
Or install it yourself as:
|
52
|
+
|
53
|
+
$ gem install strings-truncation
|
54
|
+
|
55
|
+
## 1. Usage
|
56
|
+
|
57
|
+
Use `truncate` to shorten string to 30 characters by default:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
strings = Strings::Truncation.new
|
61
|
+
strings.truncate("I try all things, I achieve what I can.")
|
62
|
+
# => "I try all things, I achieve w…"
|
63
|
+
```
|
64
|
+
|
65
|
+
As a convenience, you can call `truncate` method directly on a class:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
Strings::Truncation.truncate("I try all things, I achieve what I can.")
|
69
|
+
# => "I try all things, I achieve w…"
|
70
|
+
```
|
71
|
+
|
72
|
+
To change the default truncation length, pass an integer as a second argument:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
strings.truncate("I try all things, I achieve what I can.", 15)
|
76
|
+
# => "I try all thin…"
|
77
|
+
```
|
78
|
+
|
79
|
+
Or if you want to be more explicit and flexible use `:length` keyword:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
strings.truncate("I try all things, I achieve what I can.", length: 15)
|
83
|
+
# => "I try all thin…"
|
84
|
+
```
|
85
|
+
|
86
|
+
You can specify custom omission string in place of default `…`:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
strings.truncate("I try all things, I achieve what I can.", omission: "...")
|
90
|
+
# => "I try all things, I achieve..."
|
91
|
+
```
|
92
|
+
|
93
|
+
If you wish to truncate preserving words use a string or regexp as a separator:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
strings.truncate("I try all things, I achieve what I can.", separator: /\s/)
|
97
|
+
# => "I try all things, I achieve…"
|
98
|
+
```
|
99
|
+
|
100
|
+
You can omit text from the `start`, `middle`, `end` or both `ends`:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
strings.truncate("I try all things, I achieve what I can", position: :middle)
|
104
|
+
# => "I try all thing…ve what I can."
|
105
|
+
```
|
106
|
+
|
107
|
+
You can truncate text with fullwidth characters (Chinese, Japanese, Korean etc):
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
strings.truncate("おはようございます", 8)
|
111
|
+
# => "おはよ…"
|
112
|
+
```
|
113
|
+
|
114
|
+
As well as truncate text that contains ANSI escape codes:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
strings.truncate("\e[34mI try all things, I achieve what I can\e[0m", 18)
|
118
|
+
# => "\e[34mI try all things,\e[0m…"
|
119
|
+
```
|
120
|
+
|
121
|
+
## 2. API
|
122
|
+
|
123
|
+
### 2.1 configure
|
124
|
+
|
125
|
+
To change default configuration settings at initialization use keyword arguments.
|
126
|
+
|
127
|
+
For example, to omit text from the start and separate on a whitespace character do:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
strings = Strings::Truncation.new(position: :start, separator: /\s/)
|
131
|
+
```
|
132
|
+
|
133
|
+
After initialization, you can use `configure` to change settings inside a block:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
strings.configure do |config|
|
137
|
+
config.length 25
|
138
|
+
config.omission "[...]"
|
139
|
+
config.position :start
|
140
|
+
config.separator /\s/
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
Alternatively, you can also use `configure` with keyword arguments:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
strings.configure(position: :start, separator: /\s/)
|
148
|
+
```
|
149
|
+
|
150
|
+
### 2.2 truncate
|
151
|
+
|
152
|
+
By default a string is truncated from the end to maximum length of `30` display columns.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
strings.truncate("I try all things, I achieve what I can.")
|
156
|
+
# => "I try all things, I achieve w…"
|
157
|
+
```
|
158
|
+
|
159
|
+
To change the default truncation length, pass an integer as a second argument:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
strings.truncate("I try all things, I achieve what I can.", 15)
|
163
|
+
# => "I try all thin…"
|
164
|
+
```
|
165
|
+
|
166
|
+
Or use `:length` keyword to be more explicit:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
strings.truncate("I try all things, I achieve what I can.", length: 15)
|
170
|
+
# => "I try all thin…"
|
171
|
+
```
|
172
|
+
|
173
|
+
The default `…` omission character can be replaced using `:omission`:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
strings.truncate("I try all things, I achieve what I can.", omission: "...")
|
177
|
+
# => "I try all things, I achieve..."
|
178
|
+
```
|
179
|
+
|
180
|
+
You can omit text from the `start`, `middle`, `end` or both `ends` by specifying `:position`:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
strings.truncate("I try all things, I achieve what I can", position: :start)
|
184
|
+
# => "…things, I achieve what I can."
|
185
|
+
|
186
|
+
strings.truncate("I try all things, I achieve what I can", position: :middle)
|
187
|
+
# => "I try all thing…ve what I can."
|
188
|
+
|
189
|
+
strings.truncate("I try all things, I achieve what I can", position: :ends)
|
190
|
+
# => "… all things, I achieve what …"
|
191
|
+
```
|
192
|
+
|
193
|
+
To truncate based on custom character(s) use `:separator` that accepts a string or regular expression:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
strings.truncate("I try all things, I achieve what I can.", separator: /\s/)
|
197
|
+
=> "I try all things, I achieve…"
|
198
|
+
```
|
199
|
+
|
200
|
+
You can combine all settings to achieve desired result:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
strings.truncate("I try all things, I achieve what I can.", length: 20,
|
204
|
+
omission: "...", position: :ends, separator: /\s/)
|
205
|
+
# => "...I achieve what..."
|
206
|
+
```
|
207
|
+
|
208
|
+
## 3. Extending String class
|
209
|
+
|
210
|
+
Though it is highly discouraged to pollute core Ruby classes, you can add the required methods to String class by using refinements.
|
211
|
+
|
212
|
+
To include all the **Strings::Truncation** methods, you can load extensions like so:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
require "strings/truncation/extensions"
|
216
|
+
|
217
|
+
using Strings::Truncation::Extensions
|
218
|
+
```
|
219
|
+
|
220
|
+
And then call `truncate` directly on any string:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
"I try all things, I achieve what I can.".truncate(20, separator: " ")
|
224
|
+
# => "I try all things, I…"
|
225
|
+
```
|
226
|
+
|
227
|
+
## Development
|
228
|
+
|
229
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
230
|
+
|
231
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
232
|
+
|
233
|
+
## Contributing
|
234
|
+
|
235
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/strings-truncation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
236
|
+
|
237
|
+
## License
|
238
|
+
|
239
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
240
|
+
|
241
|
+
## Code of Conduct
|
242
|
+
|
243
|
+
Everyone interacting in the Strings::Truncation project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/strings-truncation/blob/master/CODE_OF_CONDUCT.md).
|
244
|
+
|
245
|
+
## Copyright
|
246
|
+
|
247
|
+
Copyright (c) 2019 Piotr Murach. See LICENSE for further details.
|
@@ -0,0 +1 @@
|
|
1
|
+
require "strings/truncation"
|
@@ -0,0 +1,368 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "strscan"
|
5
|
+
require "strings-ansi"
|
6
|
+
require "unicode/display_width"
|
7
|
+
|
8
|
+
require_relative "truncation/configuration"
|
9
|
+
require_relative "truncation/version"
|
10
|
+
|
11
|
+
module Strings
|
12
|
+
class Truncation
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
ANSI_REGEXP = /#{Strings::ANSI::ANSI_MATCHER}/.freeze
|
16
|
+
RESET_REGEXP = /#{Regexp.escape(Strings::ANSI::RESET)}/.freeze
|
17
|
+
END_REGEXP = /\A(#{Strings::ANSI::ANSI_MATCHER})*\z/.freeze
|
18
|
+
|
19
|
+
# Global instance
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
def self.__instance__
|
23
|
+
@__instance__ ||= Truncation.new
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
extend Forwardable
|
28
|
+
|
29
|
+
delegate %i[truncate] => :__instance__
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a Strings::Truncation instance
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# strings = Strings::Truncation.new(separator: /[,- ]/)
|
36
|
+
#
|
37
|
+
# @param [Integer] length
|
38
|
+
# the maximum length to truncate to
|
39
|
+
# @param [String] omission
|
40
|
+
# the string to denote omitted content
|
41
|
+
# @param [String|Integer] position
|
42
|
+
# the position of the omission within the string
|
43
|
+
# @param [String|Regexp] separator
|
44
|
+
# the separator to break words on
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def initialize(**options)
|
48
|
+
configuration.update(**options)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Access configuration
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def configuration
|
55
|
+
@configuration ||= Configuration.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Configure truncation
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# strings = Strings::Truncation.new
|
62
|
+
# strings.configure do |config|
|
63
|
+
# config.length 20
|
64
|
+
# config.omission "[...]"
|
65
|
+
# config.position :middle
|
66
|
+
# config.separator /[,- ]/
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# strings = Strings::Truncation.new
|
71
|
+
# strings.configure length: 20, omission: "[...]"
|
72
|
+
#
|
73
|
+
# @yield [Configuration]
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def configure(**options)
|
77
|
+
if block_given?
|
78
|
+
yield configuration
|
79
|
+
else
|
80
|
+
configuration.update(**options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Truncate a text at a given length (defualts to 30)
|
85
|
+
#
|
86
|
+
# @param [String] text
|
87
|
+
# the text to be truncated
|
88
|
+
#
|
89
|
+
# @param [Integer] truncate_at
|
90
|
+
# the width at which to truncate the text
|
91
|
+
#
|
92
|
+
# @param [String|Regexp] separator
|
93
|
+
# the character for splitting words
|
94
|
+
#
|
95
|
+
# @param [String] omission
|
96
|
+
# the string to use for displaying omitted content
|
97
|
+
#
|
98
|
+
# @param [String|Integer] position
|
99
|
+
# the position in text from which to omit content
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# truncate("It is not down on any map; true places never are.")
|
103
|
+
# # => "It is not down on any map; tr…""
|
104
|
+
#
|
105
|
+
# truncate("It is not down on any map; true places never are.", 15)
|
106
|
+
# # => "It is not down…""
|
107
|
+
#
|
108
|
+
# truncate("It is not down on any map; true places never are.",
|
109
|
+
# separator: " ")
|
110
|
+
# # => "It is not down on any map;…"
|
111
|
+
#
|
112
|
+
# truncate("It is not down on any map; true places never are.", 40,
|
113
|
+
# omission: "[...]")
|
114
|
+
# # => "It is not down on any map; true pla[...]"
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def truncate(text, truncate_at = configuration.length, length: nil,
|
118
|
+
position: configuration.position,
|
119
|
+
separator: configuration.separator,
|
120
|
+
omission: configuration.omission)
|
121
|
+
truncate_at = length if length
|
122
|
+
|
123
|
+
return text if truncate_at.nil? || text.bytesize <= truncate_at.to_i
|
124
|
+
|
125
|
+
return "" if truncate_at.zero?
|
126
|
+
|
127
|
+
separator = Regexp.new(separator) if separator
|
128
|
+
|
129
|
+
case position
|
130
|
+
when :start
|
131
|
+
truncate_start(text, truncate_at, omission, separator)
|
132
|
+
when :middle
|
133
|
+
truncate_middle(text, truncate_at, omission, separator)
|
134
|
+
when :ends
|
135
|
+
truncate_ends(text, truncate_at, omission, separator)
|
136
|
+
when :end
|
137
|
+
truncate_from(0, text, truncate_at, omission, separator)
|
138
|
+
when Numeric
|
139
|
+
truncate_from(position, text, truncate_at, omission, separator)
|
140
|
+
else
|
141
|
+
raise Error, "unsupported position: #{position.inspect}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
# Truncate text at the start
|
148
|
+
#
|
149
|
+
# @param [String] text
|
150
|
+
# the text to truncate
|
151
|
+
# @param [Integer] length
|
152
|
+
# the maximum length to truncate at
|
153
|
+
# @param [String] omission
|
154
|
+
# the string to denote omitted content
|
155
|
+
# @param [String|Regexp] separator
|
156
|
+
# the pattern or string to separate on
|
157
|
+
#
|
158
|
+
# @return [String]
|
159
|
+
# the truncated text
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
def truncate_start(text, length, omission, separator)
|
163
|
+
text_width = display_width(Strings::ANSI.sanitize(text))
|
164
|
+
omission_width = display_width(omission)
|
165
|
+
return text if text_width == length
|
166
|
+
return omission if omission_width == length
|
167
|
+
|
168
|
+
from = [text_width - length, 0].max
|
169
|
+
from += omission_width if from > 0
|
170
|
+
words, = *slice(text, from, length - omission_width,
|
171
|
+
omission_width: omission_width,
|
172
|
+
separator: separator)
|
173
|
+
|
174
|
+
"#{omission if from > 0}#{words}"
|
175
|
+
end
|
176
|
+
|
177
|
+
# Truncate text before the from position and after the length
|
178
|
+
#
|
179
|
+
# @param [Integer] from
|
180
|
+
# the position to start extracting from
|
181
|
+
# @param [String] text
|
182
|
+
# the text to truncate
|
183
|
+
# @param [Integer] length
|
184
|
+
# the maximum length to truncate at
|
185
|
+
# @param [String] omission
|
186
|
+
# the string to denote omitted content
|
187
|
+
# @param [String|Regexp] separator
|
188
|
+
# the pattern or string to separate on
|
189
|
+
#
|
190
|
+
# @return [String]
|
191
|
+
# the truncated text
|
192
|
+
#
|
193
|
+
# @api private
|
194
|
+
def truncate_from(from, text, length, omission, separator)
|
195
|
+
omission_width = display_width(omission)
|
196
|
+
length_without_omission = length - omission_width
|
197
|
+
length_without_omission -= omission_width if from > 0
|
198
|
+
words, stop = *slice(text, from, length_without_omission,
|
199
|
+
omission_width: omission_width,
|
200
|
+
separator: separator)
|
201
|
+
|
202
|
+
"#{omission if from > 0}#{words}#{omission if stop}"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Truncate text in the middle
|
206
|
+
#
|
207
|
+
# @param [String] text
|
208
|
+
# the text to truncate
|
209
|
+
# @param [Integer] length
|
210
|
+
# the maximum length to truncate at
|
211
|
+
# @param [String] omission
|
212
|
+
# the string to denote omitted content
|
213
|
+
# @param [String|Regexp] separator
|
214
|
+
# the pattern or string to separate on
|
215
|
+
#
|
216
|
+
# @return [String]
|
217
|
+
# the truncated text
|
218
|
+
#
|
219
|
+
# @api private
|
220
|
+
def truncate_middle(text, length, omission, separator)
|
221
|
+
text_width = display_width(Strings::ANSI.sanitize(text))
|
222
|
+
omission_width = display_width(omission)
|
223
|
+
return text if text_width == length
|
224
|
+
return omission if omission_width == length
|
225
|
+
|
226
|
+
half_length = length / 2
|
227
|
+
rem_length = half_length + length % 2
|
228
|
+
half_omission = omission_width / 2
|
229
|
+
rem_omission = half_omission + omission_width % 2
|
230
|
+
|
231
|
+
before_words, = *slice(text, 0, half_length - half_omission,
|
232
|
+
omission_width: half_omission,
|
233
|
+
separator: separator)
|
234
|
+
|
235
|
+
after_words, = *slice(text, text_width - rem_length + rem_omission,
|
236
|
+
rem_length - rem_omission,
|
237
|
+
omission_width: rem_omission,
|
238
|
+
separator: separator)
|
239
|
+
|
240
|
+
"#{before_words}#{omission}#{after_words}"
|
241
|
+
end
|
242
|
+
|
243
|
+
# Truncate text at both ends
|
244
|
+
#
|
245
|
+
# @param [String] text
|
246
|
+
# the text to truncate
|
247
|
+
# @param [Integer] length
|
248
|
+
# the maximum length to truncate at
|
249
|
+
# @param [String] omission
|
250
|
+
# the string to denote omitted content
|
251
|
+
# @param [String|Regexp] separator
|
252
|
+
# the pattern or string to separate on
|
253
|
+
#
|
254
|
+
# @return [String]
|
255
|
+
# the truncated text
|
256
|
+
#
|
257
|
+
# @api private
|
258
|
+
def truncate_ends(text, length, omission, separator)
|
259
|
+
text_width = display_width(Strings::ANSI.sanitize(text))
|
260
|
+
omission_width = display_width(omission)
|
261
|
+
return text if text_width <= length
|
262
|
+
return omission if length <= 2 * omission_width
|
263
|
+
|
264
|
+
from = (text_width - length) / 2 + omission_width
|
265
|
+
words, stop = *slice(text, from, length - 2 * omission_width,
|
266
|
+
omission_width: omission_width,
|
267
|
+
separator: separator)
|
268
|
+
return omission if words.empty?
|
269
|
+
|
270
|
+
"#{omission if from > 0}#{words}#{omission if stop}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# Extract number of characters from a text starting at the from position
|
274
|
+
#
|
275
|
+
# @param [Integer] from
|
276
|
+
# the position to start from
|
277
|
+
# @param [Integer] length
|
278
|
+
# the number of characters to extract
|
279
|
+
# @param [Integer] omission_width
|
280
|
+
# the width of the omission
|
281
|
+
# @param [String|Regexp] separator
|
282
|
+
# the string or pattern to use for splitting words
|
283
|
+
#
|
284
|
+
# @return [Array<String, Boolean>]
|
285
|
+
# return a substring and a stop flag
|
286
|
+
#
|
287
|
+
# @api private
|
288
|
+
def slice(text, from, length, omission_width: 0, separator: nil)
|
289
|
+
scanner = StringScanner.new(text)
|
290
|
+
length_with_omission = length + omission_width
|
291
|
+
current_length = 0
|
292
|
+
start_position = 0
|
293
|
+
ansi_reset = false
|
294
|
+
visible_char = false
|
295
|
+
word_break = false
|
296
|
+
stop = false
|
297
|
+
words = []
|
298
|
+
word = []
|
299
|
+
char = nil
|
300
|
+
|
301
|
+
while !(scanner.eos? || stop)
|
302
|
+
if scanner.scan(RESET_REGEXP)
|
303
|
+
unless scanner.eos?
|
304
|
+
words << scanner.matched
|
305
|
+
ansi_reset = false
|
306
|
+
end
|
307
|
+
elsif scanner.scan(ANSI_REGEXP)
|
308
|
+
words << scanner.matched
|
309
|
+
ansi_reset = true
|
310
|
+
else
|
311
|
+
if (char =~ separator && start_position <= from) ||
|
312
|
+
separator && start_position.zero?
|
313
|
+
word_break = start_position != from
|
314
|
+
end
|
315
|
+
|
316
|
+
char = scanner.getch
|
317
|
+
char_width = display_width(char)
|
318
|
+
start_position += char_width
|
319
|
+
next if (start_position - char_width) < from
|
320
|
+
|
321
|
+
current_length += char_width
|
322
|
+
|
323
|
+
if char =~ separator
|
324
|
+
if word_break
|
325
|
+
word_break = false
|
326
|
+
current_length = 0
|
327
|
+
next
|
328
|
+
end
|
329
|
+
visible_char = true
|
330
|
+
words << word.join
|
331
|
+
word.clear
|
332
|
+
end
|
333
|
+
|
334
|
+
if current_length <= length || scanner.check(END_REGEXP) &&
|
335
|
+
current_length <= length_with_omission
|
336
|
+
if separator
|
337
|
+
word << char unless word_break
|
338
|
+
else
|
339
|
+
words << char
|
340
|
+
visible_char = true
|
341
|
+
end
|
342
|
+
else
|
343
|
+
stop = true
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
visible_char = true if !word.empty? && scanner.eos?
|
349
|
+
|
350
|
+
return ["", stop] unless visible_char
|
351
|
+
|
352
|
+
words << word.join if !word.empty? && scanner.eos?
|
353
|
+
|
354
|
+
words << Strings::ANSI::RESET if ansi_reset
|
355
|
+
|
356
|
+
[words.join, stop]
|
357
|
+
end
|
358
|
+
|
359
|
+
# Visible width of a string
|
360
|
+
#
|
361
|
+
# @return [Integer]
|
362
|
+
#
|
363
|
+
# @api private
|
364
|
+
def display_width(string)
|
365
|
+
Unicode::DisplayWidth.of(string)
|
366
|
+
end
|
367
|
+
end # Truncation
|
368
|
+
end # Strings
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strings
|
4
|
+
class Truncation
|
5
|
+
# A configuration object used by a Strings::Truncation instance
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Configuration
|
9
|
+
DEFAULT_LENGTH = 30
|
10
|
+
|
11
|
+
DEFAULT_OMISSION = "…"
|
12
|
+
|
13
|
+
DEFAULT_POSITION = 0
|
14
|
+
|
15
|
+
# Create a configuration
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def initialize(length: DEFAULT_LENGTH, omission: DEFAULT_OMISSION,
|
19
|
+
position: DEFAULT_POSITION, separator: nil)
|
20
|
+
@length = length
|
21
|
+
@omission = omission
|
22
|
+
@position = position
|
23
|
+
@separator = separator
|
24
|
+
end
|
25
|
+
|
26
|
+
# Update current configuration
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def update(length: nil, omission: nil, position: nil, separator: nil)
|
30
|
+
@length = length if length
|
31
|
+
@omission = omission if omission
|
32
|
+
@position = position if position
|
33
|
+
@separator = separator if separator
|
34
|
+
end
|
35
|
+
|
36
|
+
# The maximum length to truncate to
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# strings = Strings::Truncation.new
|
40
|
+
#
|
41
|
+
# strings.configure do |config|
|
42
|
+
# config.length 40
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @param [Integer] number
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def length(number = (not_set = true))
|
49
|
+
if not_set
|
50
|
+
@length
|
51
|
+
else
|
52
|
+
@length = number
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# The string to denote omitted content
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# strings = Strings::Truncation.new
|
60
|
+
#
|
61
|
+
# strings.configure do |config|
|
62
|
+
# config.omission "..."
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @param [String] string
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def omission(string = (not_set = true))
|
69
|
+
if not_set
|
70
|
+
@omission
|
71
|
+
else
|
72
|
+
@omission = string
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# The position of the omission within the string
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# strings = Strings::Truncation.new
|
80
|
+
#
|
81
|
+
# strings.configure do |config|
|
82
|
+
# config.position :start
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
#
|
86
|
+
# @param [Symbol] position
|
87
|
+
# the position out of :start, :middle or :end
|
88
|
+
#
|
89
|
+
# @api public
|
90
|
+
def position(position = (not_set = true))
|
91
|
+
if not_set
|
92
|
+
@position
|
93
|
+
else
|
94
|
+
@position = position
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# The separator to break the characters into words
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# strings = Strings::Truncation.new
|
102
|
+
#
|
103
|
+
# strings.configure do |config|
|
104
|
+
# config.separator /[, ]/
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# @param [String|Regexp] separator
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def separator(separator = (not_set = true))
|
111
|
+
if not_set
|
112
|
+
@separator
|
113
|
+
else
|
114
|
+
@separator = separator
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end # Configruation
|
118
|
+
end # Truncation
|
119
|
+
end # Strings
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../truncation"
|
4
|
+
|
5
|
+
module Strings
|
6
|
+
class Truncation
|
7
|
+
module Extensions
|
8
|
+
refine String do
|
9
|
+
def truncate(*args, **options)
|
10
|
+
Strings::Truncation.truncate(self, *args, **options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end # Extensions
|
14
|
+
end # Truncation
|
15
|
+
end # Strings
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: strings-truncation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Piotr Murach
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: strings-ansi
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: unicode-display_width
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3.0'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.6'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.0'
|
75
|
+
description: Truncate strings with fullwidth characters and ANSI codes. Characters
|
76
|
+
can be omitted from the start, middle, end or both ends.
|
77
|
+
email:
|
78
|
+
- piotr@piotrmurach.com
|
79
|
+
executables: []
|
80
|
+
extensions: []
|
81
|
+
extra_rdoc_files:
|
82
|
+
- README.md
|
83
|
+
- CHANGELOG.md
|
84
|
+
- LICENSE.txt
|
85
|
+
files:
|
86
|
+
- CHANGELOG.md
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.md
|
89
|
+
- lib/strings-truncation.rb
|
90
|
+
- lib/strings/truncation.rb
|
91
|
+
- lib/strings/truncation/configuration.rb
|
92
|
+
- lib/strings/truncation/extensions.rb
|
93
|
+
- lib/strings/truncation/version.rb
|
94
|
+
homepage: https://github.com/piotrmurach/strings-truncation
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata:
|
98
|
+
allowed_push_host: https://rubygems.org
|
99
|
+
bug_tracker_uri: https://github.com/piotrmurach/strings-truncation/issues
|
100
|
+
changelog_uri: https://github.com/piotrmurach/strings-truncation/blob/master/CHANGELOG.md
|
101
|
+
documentation_uri: https://www.rubydoc.info/gems/strings-truncation
|
102
|
+
homepage_uri: https://github.com/piotrmurach/strings-truncation
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 2.0.0
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubygems_version: 3.1.2
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Truncate strings with fullwidth characters and ANSI codes.
|
122
|
+
test_files: []
|