strings-truncation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 Version](https://badge.fury.io/rb/strings-truncation.svg)][gem]
|
8
|
+
[![Actions CI](https://github.com/piotrmurach/strings-truncation/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
|
9
|
+
[![Build status](https://ci.appveyor.com/api/projects/status/s8y94c4tvi8mgrh2?svg=true)][appveyor]
|
10
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/f7ecb5bf87696e522ccb/maintainability)][codeclimate]
|
11
|
+
[![Coverage Status](https://coveralls.io/repos/github/piotrmurach/strings-truncation/badge.svg?branch=master)][coverage]
|
12
|
+
[![Inline docs](http://inch-ci.org/github/piotrmurach/strings-truncation.svg?branch=master)][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: []
|