ansi_string 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d27e27a89f1bc2e725db1275b73b741dda60878
4
+ data.tar.gz: 65b257a825eb8e50fcba397647596d01164b7cae
5
+ SHA512:
6
+ metadata.gz: a3d1a8dc6aa9bbcca32192a2268d46e2614ed50cf35ee8a5eb5f6ed81f10218da9c56a2297e7a3d92147922c01d32bf96a5530f7ee489fc28aa34240ffa474a6
7
+ data.tar.gz: 1d910cfd8d008811ac332ed8569db14dc8125ce2dc49f05e602aa769aa3a20630c64ce4b8fb89624b28c2448b38cacd45fbca244fd5fcf9b6fdd975e6eb5b591
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ansi_string.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Zach Dennis
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.
@@ -0,0 +1,41 @@
1
+ # AnsiString
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ansi_string`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ansi_string'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ansi_string
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ansi_string.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ansi_string/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ansi_string"
8
+ spec.version = AnsiString::VERSION
9
+ spec.authors = ["Zach Dennis"]
10
+ spec.email = ["zach.dennis@gmail.com"]
11
+
12
+ spec.summary = %q{ANSIString is a String library that is ANSI-sequence-aware}
13
+ spec.description = %q{ANSIString is a String library that is ANSI-sequence-aware}
14
+ spec.homepage = "https://github.com/zdennis/ansi_string"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.12"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency "term-ansicolor", "~> 1.3"
26
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ansi_string"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,371 @@
1
+ # encoding: UTF-8
2
+ require 'ansi_string/version'
3
+ require 'forwardable'
4
+
5
+ class ANSIString
6
+ extend Forwardable
7
+ attr_reader :raw, :without_ansi
8
+
9
+ def_delegators :@without_ansi, :each_char, :each_byte, :index,
10
+ :match, :=~
11
+
12
+ def initialize(str)
13
+ process_string raw_string_for(str)
14
+ end
15
+
16
+ def +(other)
17
+ self.class.new @raw + raw_string_for(other)
18
+ end
19
+
20
+ def <<(other)
21
+ range = length..length
22
+ str = replace_in_string(range, other)
23
+ process_string raw_string_for(str)
24
+ self
25
+ end
26
+
27
+ def insert(position, string)
28
+ if position < 0
29
+ position = @without_ansi.length + position + 1
30
+ end
31
+ self[position...position] = string
32
+ self
33
+ end
34
+
35
+ def empty?
36
+ length == 0
37
+ end
38
+
39
+ def [](range)
40
+ # convert numeric position to a range
41
+ range = (range..range) if range.is_a?(Integer)
42
+
43
+ range_begin = range.begin
44
+ range_end = range.end
45
+
46
+ if range.exclude_end?
47
+ if range_begin == 0 && range_end == 0
48
+ return ""
49
+ else
50
+ range_end -= 1
51
+ end
52
+ end
53
+
54
+ range_begin = @without_ansi.length - range.begin.abs if range.begin < 0
55
+ range_end = @without_ansi.length - range.end.abs if range.end < 0
56
+
57
+ str = build_string_with_ansi_for(range_begin..range_end)
58
+ ANSIString.new str if str
59
+ end
60
+
61
+ def []=(range, replacement_str)
62
+ # convert numeric position to a range
63
+ range = (range..range) if range.is_a?(Integer)
64
+
65
+ range_begin = range.begin
66
+ range_end = range.exclude_end? ? range.end - 1 : range.end
67
+
68
+ range_begin = @without_ansi.length - range.begin.abs if range.begin < 0
69
+ range_end = @without_ansi.length - range.end.abs if range.end < 0
70
+
71
+ updated_string = replace_in_string(range_begin..range_end, replacement_str)
72
+ process_string raw_string_for(updated_string)
73
+ self
74
+ end
75
+
76
+ # See String#rindex for arguments
77
+ def rindex(*args)
78
+ @without_ansi.rindex(*args)
79
+ end
80
+
81
+ def replace(str)
82
+ process_string raw_string_for(str)
83
+ self
84
+ end
85
+
86
+ def reverse
87
+ str = @ansi_sequence_locations.reverse.map do |location|
88
+ [location[:start_ansi_sequence], location[:text].reverse, location[:end_ansi_sequence]].join
89
+ end.join
90
+ ANSIString.new str
91
+ end
92
+
93
+ def scan(pattern)
94
+ results = []
95
+ without_ansi.enum_for(:scan, pattern).each do
96
+ md = Regexp.last_match
97
+ if md.captures.any?
98
+ results << md.captures.map.with_index do |_, i|
99
+ # captures use 1-based indexing
100
+ self[md.begin(i+1)..md.end(i+1)-1]
101
+ end
102
+ else
103
+ results << self[md.begin(0)..md.end(0)-1]
104
+ end
105
+ end
106
+ results
107
+ end
108
+
109
+ def slice(index, length=nil)
110
+ return ANSIString.new("") if length == 0
111
+ range = nil
112
+ index = index.without_ansi if index.is_a?(ANSIString)
113
+ index = Regexp.new Regexp.escape(index) if index.is_a?(String)
114
+ if index.is_a?(Integer)
115
+ length ||= 1
116
+ range = (index..index+length-1)
117
+ elsif index.is_a?(Range)
118
+ range = index
119
+ elsif index.is_a?(Regexp)
120
+ md = @without_ansi.match(index)
121
+ capture_group_index = length || 0
122
+ if md
123
+ capture_group = md.offset(capture_group_index)
124
+ range = (capture_group.first..capture_group.last-1)
125
+ end
126
+ else
127
+ raise(ArgumentError, "Must pass in at least an index or a range.")
128
+ end
129
+ self[range] if range
130
+ end
131
+
132
+ def split(*args)
133
+ raw.split(*args).map { |s| ANSIString.new(s) }
134
+ end
135
+
136
+ def strip
137
+ ANSIString.new raw.strip
138
+ end
139
+
140
+ def length
141
+ @without_ansi.length
142
+ end
143
+
144
+ def lines
145
+ result = []
146
+ current_string = ""
147
+ @ansi_sequence_locations.map do |location|
148
+ if location[:text] == "\n"
149
+ result << ANSIString.new(current_string + "\n")
150
+ current_string = ""
151
+ next
152
+ end
153
+
154
+ location[:text].scan(/.*(?:\n|$)/).each_with_index do |line, i|
155
+ break if line == ""
156
+
157
+ if i == 0
158
+ current_string << [
159
+ location[:start_ansi_sequence],
160
+ line,
161
+ location[:end_ansi_sequence]
162
+ ].join
163
+ else
164
+ result << ANSIString.new(current_string)
165
+ current_string = ""
166
+ current_string << [
167
+ location[:start_ansi_sequence],
168
+ line,
169
+ location[:end_ansi_sequence]
170
+ ].join
171
+ end
172
+ end
173
+
174
+ if location[:text].end_with?("\n")
175
+ result << ANSIString.new(current_string)
176
+ current_string = ""
177
+ next
178
+ end
179
+ end
180
+ result << ANSIString.new(current_string) if current_string.length > 0
181
+ result
182
+ end
183
+
184
+ def dup
185
+ ANSIString.new(@raw.dup)
186
+ end
187
+
188
+ def sub(pattern, replacement)
189
+ str = ""
190
+ count = 0
191
+ max_count = 1
192
+ index = 0
193
+ @without_ansi.enum_for(:scan, pattern).each do
194
+ md = Regexp.last_match
195
+ str << build_string_with_ansi_for(index...(index + md.begin(0)))
196
+ index = md.end(0)
197
+ break if (count += 1) == max_count
198
+ end
199
+ if index != @without_ansi.length
200
+ str << build_string_with_ansi_for(index..@without_ansi.length)
201
+ end
202
+ nstr = str.gsub /(\033\[[0-9;]*m)(.+?)\033\[0m\1/, '\1\2'
203
+ ANSIString.new(nstr)
204
+ end
205
+
206
+ def to_s
207
+ @raw.dup
208
+ end
209
+ alias :to_str :to_s
210
+
211
+ def inspect
212
+ to_s.inspect
213
+ end
214
+
215
+ def ==(other)
216
+ (other.class == self.class && other.raw == @raw) || (other.kind_of?(String) && other == @raw)
217
+ end
218
+
219
+ def <=>(other)
220
+ (other.class == self.class && @raw <=> other.raw)
221
+ end
222
+
223
+ private
224
+
225
+ def raw_string_for(str)
226
+ str.is_a?(ANSIString) ? str.raw : str.to_s
227
+ end
228
+
229
+ def process_string(raw_str)
230
+ @without_ansi = ""
231
+ @ansi_sequence_locations = []
232
+ raw_str.enum_for(:scan, /(\e\[[0-9;]*m)?(.*?)(?=\e\[[0-9;]*m|\Z)/m ).each do
233
+ md = Regexp.last_match
234
+ ansi_sequence, text = md.captures
235
+
236
+ previous_sequence_location = @ansi_sequence_locations.last
237
+ if previous_sequence_location
238
+ if ansi_sequence == "\e[0m"
239
+ previous_sequence_location[:end_ansi_sequence] = ansi_sequence
240
+ ansi_sequence = nil
241
+ elsif previous_sequence_location[:start_ansi_sequence] == ansi_sequence
242
+ previous_sequence_location[:text] << text
243
+ previous_sequence_location[:ends_at] += text.length
244
+ previous_sequence_location[:length] += text.length
245
+ @without_ansi << text
246
+ next
247
+ end
248
+ end
249
+
250
+ if ansi_sequence.nil? && text.to_s.length == 0
251
+ next
252
+ end
253
+
254
+ @ansi_sequence_locations.push(
255
+ begins_at: @without_ansi.length,
256
+ ends_at: [@without_ansi.length + text.length - 1, 0].max,
257
+ length: text.length,
258
+ text: text,
259
+ start_ansi_sequence: ansi_sequence
260
+ )
261
+
262
+ @without_ansi << text
263
+ end
264
+
265
+ @raw = @ansi_sequence_locations.map do |location|
266
+ [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].compact.join
267
+ end.join
268
+
269
+ @ansi_sequence_locations
270
+ end
271
+
272
+ def replace_in_string(range, replacement_str)
273
+ raise RangeError, "#{range.inspect} out of range" if range.begin > length
274
+ return replacement_str if @ansi_sequence_locations.empty?
275
+
276
+ range = range.begin..(range.end - 1) if range.exclude_end?
277
+ str = ""
278
+ @ansi_sequence_locations.each_with_index do |location, j|
279
+ # If the given range encompasses part of the location, then we want to
280
+ # include the whole location
281
+ if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
282
+ end_index = range.end - location[:begins_at] + 1
283
+
284
+ str << [
285
+ location[:start_ansi_sequence],
286
+ replacement_str,
287
+ location[:text][end_index..-1],
288
+ location[:end_ansi_sequence]
289
+ ].join
290
+
291
+ # If the location falls within the given range then make sure we pull
292
+ # out the bits that we want, and keep ANSI escape sequenece intact while
293
+ # doing so.
294
+ elsif location[:begins_at] <= range.begin && location[:ends_at] >= range.end
295
+ start_index = range.begin - location[:begins_at]
296
+ end_index = range.end - location[:begins_at] + 1
297
+
298
+ str << [
299
+ location[:start_ansi_sequence],
300
+ location[:text][0...start_index],
301
+ replacement_str,
302
+ location[:text][end_index..-1],
303
+ location[:end_ansi_sequence]
304
+ ].join
305
+
306
+ elsif location[:ends_at] == range.begin
307
+ start_index = range.begin - location[:begins_at]
308
+ end_index = range.end
309
+ num_chars_to_remove_from_next_location = range.end - location[:ends_at]
310
+
311
+ str << [
312
+ location[:start_ansi_sequence],
313
+ location[:text][location[:begins_at]...(location[:begins_at]+start_index)],
314
+ replacement_str,
315
+ location[:text][end_index..-1],
316
+ location[:end_ansi_sequence],
317
+ ].join
318
+
319
+ if location=@ansi_sequence_locations[j+1]
320
+ old = location.dup
321
+ location[:text][0...num_chars_to_remove_from_next_location] = ""
322
+ location[:begins_at] += num_chars_to_remove_from_next_location
323
+ location[:ends_at] += num_chars_to_remove_from_next_location
324
+ end
325
+
326
+ # If we're pushing onto the end of the string
327
+ elsif range.begin == length && location[:ends_at] == length - 1
328
+ if replacement_str.is_a?(ANSIString)
329
+ str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence], replacement_str].join
330
+ else
331
+ str << [location[:start_ansi_sequence], location[:text], replacement_str, location[:end_ansi_sequence]].join
332
+ end
333
+ else
334
+ str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
335
+ end
336
+ end
337
+
338
+ str
339
+ end
340
+
341
+ def build_string_with_ansi_for(range)
342
+ return nil if range.begin > length
343
+
344
+ str = ""
345
+
346
+ if range.exclude_end?
347
+ range = range.begin..(range.end - 1)
348
+ end
349
+
350
+ @ansi_sequence_locations.each do |location|
351
+ # If the given range encompasses part of the location, then we want to
352
+ # include the whole location
353
+ if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
354
+ str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
355
+
356
+ elsif location[:begins_at] >= range.begin && location[:begins_at] <= range.end
357
+ str << [location[:start_ansi_sequence], location[:text][0..(range.end - location[:begins_at])], location[:end_ansi_sequence]].join
358
+
359
+ # If the location falls within the given range then make sure we pull
360
+ # out the bits that we want, and keep ANSI escape sequenece intact while
361
+ # doing so.
362
+ elsif (location[:begins_at] <= range.begin && location[:ends_at] >= range.end) || range.cover?(location[:ends_at])
363
+ start_index = range.begin - location[:begins_at]
364
+ end_index = range.end - location[:begins_at]
365
+ str << [location[:start_ansi_sequence], location[:text][start_index..end_index], location[:end_ansi_sequence]].join
366
+ end
367
+ end
368
+ str
369
+ end
370
+
371
+ end
@@ -0,0 +1,3 @@
1
+ class AnsiString
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ansi_string
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zach Dennis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: term-ansicolor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ description: ANSIString is a String library that is ANSI-sequence-aware
70
+ email:
71
+ - zach.dennis@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - ansi_string.gemspec
84
+ - bin/console
85
+ - bin/setup
86
+ - lib/ansi_string.rb
87
+ - lib/ansi_string/version.rb
88
+ homepage: https://github.com/zdennis/ansi_string
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.5.1
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: ANSIString is a String library that is ANSI-sequence-aware
112
+ test_files: []
113
+ has_rdoc: