howzit 1.2.10 → 1.2.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fba37c5c428aeab92ed337697a52323d5b98d5f9398776187e000043e3a5173b
4
- data.tar.gz: 6f3a3ebfe50f50476837c2d702cd1c9854d49ff4960c51a06641e79d88abb410
3
+ metadata.gz: 36a27d1566deff8857d39035c0e40f696a29d78b7ae24274a73ceacc54541321
4
+ data.tar.gz: 7b13d6644bf397e10f629689b159102a33dd5aa2c3610f368819de3dc8736007
5
5
  SHA512:
6
- metadata.gz: 015bc504dca39d5b7a32b6d81a4df037abf61467f58b392e88389557f38a2abfb078fd3dc5ced7411b5d1654479d5a79b94da843f8384d4cf77216e9efa1c24c
7
- data.tar.gz: c0b2003e8c1b24efa1e193520a66e01813fab6e9d8e21318a7d0c82718540fa8ef1c99b075d40dc07254c678015ca3fe687b64a2be6f8ce541c22e786ac63c9a
6
+ metadata.gz: 9da1ea212df950e75a34006a44b0ade09c02fd4fe7c02ea0fddfffcf2936e6ca5db0d7f0c9e6cc44aac3d65eb4544aa9d4c03c156487f9311f39f08ee12199d4
7
+ data.tar.gz: 99f9be58ff9184a2f96e7db6fed83e3fc19f0945e31c9d58daa32db022b175a8c99a329a9c023528af52100f46583ac892091eb04ed5d9e9e791087fe8f8c82d
data/.gitignore CHANGED
@@ -41,3 +41,4 @@ Gemfile.lock
41
41
 
42
42
  # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
43
43
  .rvmrc
44
+ results.log
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### 1.2.13
2
+
3
+ 2022-08-01 20:50
4
+
5
+ ### 1.2.12
6
+
7
+ 2022-08-01 16:23
8
+
9
+ #### IMPROVED
10
+
11
+ - Replace ANSI escape codes with color template system
12
+ - When @including an external file, if the file doesn't contain any level 2+ headers, import it as plain text.
13
+
14
+ ### 1.2.11
15
+
16
+ 2022-08-01 08:23
17
+
18
+ #### IMPROVED
19
+
20
+ - Code cleanup and refactoring
21
+
1
22
  ### 1.2.10
2
23
 
3
24
  2022-08-01 07:45
@@ -6,10 +27,6 @@
6
27
 
7
28
  - Headline formatting when iTerm markers are inserted
8
29
 
9
- ### 1.2.9
10
-
11
- 2022-08-01 07:09
12
-
13
30
  ### 1.2.8
14
31
 
15
32
  2022-08-01 07:01
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in howzit.gemspec.
data/Guardfile CHANGED
@@ -1,4 +1,6 @@
1
- scope groups: [:doc, :lint, :unit]
1
+ # frozen_string_literal: true
2
+
3
+ scope groups: %i[doc lint unit]
2
4
 
3
5
  group :doc do
4
6
  guard :yard do
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Howzit
2
2
 
3
3
  [![Gem](https://img.shields.io/gem/v/howzit.svg)](https://rubygems.org/gems/howzit)
4
+ [![Travis](https://img.shields.io/travis/makenew/ruby-gem.svg)](https://travis-ci.org/makenew/ruby-gem)
4
5
  [![GitHub license](https://img.shields.io/github/license/ttscoff/howzit.svg)](./LICENSE.txt)
5
6
 
6
7
  A command-line reference tool for tracking project build systems
data/Rakefile CHANGED
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bump/tasks'
2
4
  require 'bundler/gem_tasks'
3
5
  require 'rspec/core/rake_task'
4
6
  require 'rubocop/rake_task'
5
7
  require 'yard'
6
8
 
7
- task default: [:test, :yard]
9
+ task default: %i[test yard]
8
10
 
9
11
  desc 'Run test suite'
10
- task test: [:rubocop, :spec]
12
+ task test: %i[rubocop spec]
11
13
 
12
14
  RSpec::Core::RakeTask.new
13
15
 
@@ -16,3 +18,19 @@ RuboCop::RakeTask.new do |t|
16
18
  end
17
19
 
18
20
  YARD::Rake::YardocTask.new
21
+
22
+ desc 'Development version check'
23
+ task :ver do
24
+ gver = `git ver`
25
+ cver = IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
26
+ res = `grep VERSION lib/howzit/version.rb`
27
+ version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
28
+ puts "git tag: #{gver}"
29
+ puts "version.rb: #{version}"
30
+ puts "changelog: #{cver}"
31
+ end
32
+
33
+ desc 'Changelog version check'
34
+ task :cver do
35
+ puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
36
+ end
data/bin/howzit CHANGED
@@ -1,6 +1,8 @@
1
- #!/usr/bin/env ruby
1
+ #!/usr/bin/env ruby -W1
2
2
  # frozen_string_literal: true
3
+
3
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
4
5
  require 'howzit'
5
6
 
6
- Howzit::BuildNotes.new(ARGV)
7
+ how = Howzit::BuildNotes.new(ARGV)
8
+ how.process
data/howzit.gemspec CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency 'rubocop', '~> 0.28'
32
32
  spec.add_development_dependency 'rspec', '~> 3.1'
33
33
  spec.add_development_dependency 'simplecov', '~> 0.9'
34
- spec.add_development_dependency 'codecov', '~> 0.1'
34
+ # spec.add_development_dependency 'codecov', '~> 0.1'
35
35
  spec.add_development_dependency 'fuubar', '~> 2.0'
36
36
 
37
37
  spec.add_development_dependency 'yard', '~> 0.9.5'
@@ -2,8 +2,9 @@ module Howzit
2
2
  # Primary Class for this module
3
3
  class BuildNotes
4
4
  include Prompt
5
+ include Color
5
6
 
6
- attr_accessor :arguments, :metadata
7
+ attr_accessor :cli_args, :options, :arguments, :metadata
7
8
 
8
9
  def topics
9
10
  @topics ||= read_help
@@ -40,8 +41,8 @@ module Howzit
40
41
  # favoring environment settings
41
42
  def which_pager
42
43
  if @options[:pager] =~ /auto/i
43
- pagers = [ENV['GIT_PAGER'], ENV['PAGER'],
44
- 'bat', 'less', 'more', 'cat', 'pager']
44
+ pagers = [ENV['PAGER'], ENV['GIT_PAGER'],
45
+ 'bat', 'less', 'more', 'pager']
45
46
  pagers.delete_if(&:nil?).select!(&:available?)
46
47
  return nil if pagers.empty?
47
48
 
@@ -145,12 +146,12 @@ module Howzit
145
146
  choices.each do |choice|
146
147
  case choice
147
148
  when /[A-Z]/
148
- out.push("\e[1;32m#{choice}\e[0;32m")
149
+ out.push(Color.template("{bg}#{choice}{xg}"))
149
150
  else
150
- out.push(choice)
151
+ out.push(Color.template("{w}#{choice}"))
151
152
  end
152
153
  end
153
- "\e[0;32m[#{out.join('/')}]\e[0m"
154
+ Color.template("{g}[#{out.join('/')}{g}]{x}")
154
155
  end
155
156
 
156
157
  # Create a buildnotes skeleton
@@ -159,30 +160,40 @@ module Howzit
159
160
  warn "\nCanceled"
160
161
  exit!
161
162
  end
163
+ default = !$stdout.isatty || @options[:default]
162
164
  # First make sure there isn't already a buildnotes file
163
165
  if note_file
164
- fname = "\e[1;33m#{note_file}\e[1;37m"
165
- res = yn("#{fname} exists and appears to be a build note, continue anyway?", false)
166
- unless res
167
- puts 'Canceled'
168
- Process.exit 0
166
+ fname = Color.template("{by}#{note_file}{bw}")
167
+ unless default
168
+ res = yn("#{fname} exists and appears to be a build note, continue anyway?", false)
169
+ unless res
170
+ puts 'Canceled'
171
+ Process.exit 0
172
+ end
169
173
  end
170
174
  end
171
175
 
172
176
  title = File.basename(Dir.pwd)
173
- printf "\e[1;37mProject name \e[0;32m[#{title}]\e[1;37m: \e[0m"
174
- input = STDIN.gets.chomp
175
- title = input unless input.empty?
176
-
177
+ if default
178
+ input = title
179
+ else
180
+ printf Color.template("{bw}Project name {xg}[#{title}]{bw}: {x}")
181
+ input = $stdin.gets.chomp
182
+ title = input unless input.empty?
183
+ end
177
184
  summary = ''
178
- printf "\e[1;37mProject summary: \e[0m"
179
- input = STDIN.gets.chomp
180
- summary = input unless input.empty?
185
+ unless default
186
+ printf Color.template('{bw}Project summary: {x}')
187
+ input = $stdin.gets.chomp
188
+ summary = input unless input.empty?
189
+ end
181
190
 
182
- filename = 'buildnotes.md'
183
- printf "\e[1;37mBuild notes filename (must begin with 'howzit' or 'build')\n\e[0;32m[#{filename}]\e[1;37m: \e[0m"
184
- input = STDIN.gets.chomp
185
- filename = input unless input.empty?
191
+ fname = 'buildnotes.md'
192
+ unless default
193
+ printf Color.template("{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}")
194
+ input = $stdin.gets.chomp
195
+ fname = input unless input.empty?
196
+ end
186
197
 
187
198
  note = <<~EOBUILDNOTES
188
199
  # #{title}
@@ -209,8 +220,8 @@ module Howzit
209
220
 
210
221
  EOBUILDNOTES
211
222
 
212
- if File.exist?(filename)
213
- file = "\e[1;33m#{filename}"
223
+ if File.exist?(fname) && !default
224
+ file = Color.template("{by}#{fname}")
214
225
  res = yn("Are you absolutely sure you want to overwrite #{file}", false)
215
226
 
216
227
  unless res
@@ -219,9 +230,9 @@ module Howzit
219
230
  end
220
231
  end
221
232
 
222
- File.open(filename, 'w') do |f|
233
+ File.open(fname, 'w') do |f|
223
234
  f.puts note
224
- puts "Build notes for #{title} written to #{filename}"
235
+ puts Color.template("{by}Build notes for #{title} written to #{fname}")
225
236
  end
226
237
  end
227
238
 
@@ -229,8 +240,8 @@ module Howzit
229
240
  def format_header(title, opts = {})
230
241
  options = {
231
242
  hr: "\u{254C}",
232
- color: '1;32',
233
- border: '0',
243
+ color: '{bg}',
244
+ border: '{x}',
234
245
  mark: false
235
246
  }
236
247
 
@@ -239,29 +250,29 @@ module Howzit
239
250
  cols = TTY::Screen.columns
240
251
 
241
252
  cols = @options[:wrap] if (@options[:wrap]).positive? && cols > @options[:wrap]
242
- title = "\e[#{options[:border]}m#{options[:hr] * 2}( \e[#{options[:color]}m#{title}\e[#{options[:border]}m )"
253
+ title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
243
254
 
244
255
  tail = if should_mark_iterm?
245
256
  "#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
246
257
  else
247
258
  options[:hr] * (cols - title.uncolor.length)
248
259
  end
249
- "#{title}#{tail}\e[0m"
260
+ Color.template("#{title}#{tail}{x}")
250
261
  end
251
262
 
252
263
  def os_open(command)
253
264
  os = RbConfig::CONFIG['target_os']
254
- out = "\e[1;32mOpening \e[3;37m#{command}"
265
+ out = Color.template("{bg}Opening {bw}#{command}")
255
266
  case os
256
267
  when /darwin.*/i
257
- warn "#{out} (macOS)\e[0m" if @options[:log_level] < 2
268
+ warn Color.template("#{out} (macOS){x}") if @options[:log_level] < 2
258
269
  `open #{Shellwords.escape(command)}`
259
270
  when /mingw|mswin/i
260
- warn "#{out} (Windows)\e[0m" if @options[:log_level] < 2
271
+ warn Color.template("#{out} (Windows){x}") if @options[:log_level] < 2
261
272
  `start #{Shellwords.escape(command)}`
262
273
  else
263
274
  if 'xdg-open'.available?
264
- warn "#{out} (Linux)\e[0m" if @options[:log_level] < 2
275
+ warn Color.template("#{out} (Linux){x}") if @options[:log_level] < 2
265
276
  `xdg-open #{Shellwords.escape(command)}`
266
277
  else
267
278
  warn out if @options[:log_level] < 2
@@ -300,7 +311,7 @@ module Howzit
300
311
  directives.each do |c|
301
312
  if c[0].nil?
302
313
  title = c[3] ? c[3].strip : ''
303
- warn "\e[1;32mRunning block \e[3;37m#{title}\e[0m" if @options[:log_level] < 2
314
+ warn Color.template("{bg}Running block {bw}#{title}{x}") if @options[:log_level] < 2
304
315
  block = c[4].strip
305
316
  script = Tempfile.new('howzit_script')
306
317
  begin
@@ -322,18 +333,18 @@ module Howzit
322
333
  warn "No topic match for @include(#{search})"
323
334
  else
324
335
  if @included.include?(matches[0])
325
- warn "\e[1;33mTasks from \e[3;37m#{matches[0]} already included, skipping\e[0m" if @options[:log_level] < 2
336
+ warn Color.template("{by}Tasks from {bw}#{matches[0]} already included, skipping{x}") if @options[:log_level] < 2
326
337
  else
327
- warn "\e[1;33mIncluding tasks from \e[3;37m#{matches[0]}\e[0m" if @options[:log_level] < 2
338
+ warn Color.template("{by}Including tasks from {bw}#{matches[0]}{x}") if @options[:log_level] < 2
328
339
  process_topic(matches[0], true)
329
- warn "\e[1;33mEnd include \e[3;37m#{matches[0]}\e[0m" if @options[:log_level] < 2
340
+ warn Color.template("{by}End include {bw}#{matches[0]}{x}") if @options[:log_level] < 2
330
341
  end
331
342
  end
332
343
  when /run/i
333
- warn "\e[1;32mRunning \e[3;37m#{obj}\e[0m" if @options[:log_level] < 2
344
+ warn Color.template("{bg}Running {bw}#{obj}{x}") if @options[:log_level] < 2
334
345
  system(obj)
335
346
  when /copy/i
336
- warn "\e[1;32mCopied \e[3;37m#{obj}\e[1;32m to clipboard\e[0m" if @options[:log_level] < 2
347
+ warn Color.template("{bg}Copied {bw}#{obj}{bg} to clipboard{x}") if @options[:log_level] < 2
337
348
  `echo #{Shellwords.escape(obj)}'\\c'|pbcopy`
338
349
  when /open|url/i
339
350
  os_open(obj)
@@ -341,7 +352,7 @@ module Howzit
341
352
  end
342
353
  end
343
354
  else
344
- warn "\e[0;31m--run: No \e[1;31m@directive\e[0;31;40m found in \e[1;37m#{key}\e[0m"
355
+ warn Color.template("{r}--run: No {br}@directive{xr} found in {bw}#{key}{x}")
345
356
  end
346
357
  output.push("Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}") if @options[:log_level] < 2
347
358
 
@@ -371,12 +382,12 @@ module Howzit
371
382
  unless matches.empty?
372
383
  if opt[:single]
373
384
  title = "From #{matches[0]}:"
374
- color = '33;40'
375
- rule = '30;40'
385
+ color = '{yK}'
386
+ rule = '{kK}'
376
387
  else
377
388
  title = "Include #{matches[0]}"
378
- color = '33;40'
379
- rule = '0'
389
+ color = '{yK}'
390
+ rule = '{x}'
380
391
  end
381
392
  output.push(format_header("#{'> ' * @nest_level}#{title}", { color: color, hr: '.', border: rule })) unless @included.include?(matches[0])
382
393
 
@@ -405,15 +416,15 @@ module Howzit
405
416
  when /open|url/
406
417
  "\u{279A}"
407
418
  end
408
- output.push("\e[1;35;40m#{icon} \e[3;37;40m#{obj}\e[0m")
419
+ output.push(Color.template("{bmK}#{icon} {bwK}#{obj}{x}"))
409
420
  when /(`{3,})run *(.*?)$/i
410
421
  m = Regexp.last_match
411
422
  desc = m[2].length.positive? ? "Block: #{m[2]}" : 'Code Block'
412
- output.push("\e[1;35;40m\u{25B6} \e[3;37;40m#{desc}\e[0m\n```")
423
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}\n```"))
413
424
  when /@@@run *(.*?)$/i
414
425
  m = Regexp.last_match
415
426
  desc = m[1].length.positive? ? "Block: #{m[1]}" : 'Code Block'
416
- output.push("\e[1;35;40m\u{25B6} \e[3;37;40m#{desc}\e[0m")
427
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}"))
417
428
  else
418
429
  l.wrap!(@options[:wrap]) if (@options[:wrap]).positive?
419
430
  output.push(l)
@@ -444,9 +455,9 @@ module Howzit
444
455
  # Output a list of topic titles
445
456
  def list_topics
446
457
  output = []
447
- output.push("\e[1;32mTopics:\e[0m\n")
458
+ output.push(Color.template("{bg}Topics:{x}\n"))
448
459
  topics.each_key do |title|
449
- output.push("- \e[1;37m#{title}\e[0m")
460
+ output.push(Color.template("- {bw}#{title}{x}"))
450
461
  end
451
462
  output.join("\n")
452
463
  end
@@ -456,14 +467,14 @@ module Howzit
456
467
  topics.keys.join("\n")
457
468
  end
458
469
 
459
- def get_note_title(filename, truncate = 0)
470
+ def get_note_title(truncate = 0)
460
471
  title = nil
461
- help = IO.read(filename).strip
472
+ help = IO.read(note_file).strip
462
473
  title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
463
474
  title = if title
464
475
  title[1].nil? ? title[2] : title[1]
465
476
  else
466
- filename.sub(/(\.\w+)?$/, '')
477
+ note_file.sub(/(\.\w+)?$/, '')
467
478
  end
468
479
 
469
480
  title && truncate.positive? ? title.trunc(truncate) : title
@@ -486,7 +497,7 @@ module Howzit
486
497
 
487
498
  def list_runnable
488
499
  output = []
489
- output.push(%(\e[1;32m"Runnable" Topics:\e[0m\n))
500
+ output.push(Color.template(%({bg}"Runnable" Topics:{x}\n)))
490
501
  topics.each do |title, sect|
491
502
  s_out = []
492
503
  lines = sect.split(/\n/)
@@ -507,7 +518,7 @@ module Howzit
507
518
  end
508
519
  end
509
520
  unless s_out.empty?
510
- output.push("- \e[1;37m#{title}\e[0m")
521
+ output.push(Color.template("- {bw}#{title}{x}"))
511
522
  output.push(s_out.join("\n"))
512
523
  end
513
524
  end
@@ -531,8 +542,8 @@ module Howzit
531
542
  required = t_meta['required'].strip.split(/\s*,\s*/)
532
543
  required.each do |req|
533
544
  unless @metadata.keys.include?(req.downcase)
534
- warn %(\e[0;31mERROR: Missing required metadata key from template '\e[1;37m#{File.basename(template, '.md')}\e[0;31m'\e[0m)
535
- warn %(\e[0;31mPlease define \e[1;33m#{req.downcase}\e[0;31m in build notes\e[0m)
545
+ warn Color.template(%({xr}ERROR: Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}))
546
+ warn Color.template(%({xr}Please define {by}#{req.downcase}{xr} in build notes{x}))
536
547
  Process.exit 1
537
548
  end
538
549
  end
@@ -581,6 +592,24 @@ module Howzit
581
592
  template_topics
582
593
  end
583
594
 
595
+ def include_file(m)
596
+ file = File.expand_path(m[1])
597
+
598
+ return m[0] unless File.exist?(file)
599
+
600
+ content = IO.read(file)
601
+ home = ENV['HOME']
602
+ short_path = File.dirname(file.sub(/^#{home}/, '~'))
603
+ prefix = "#{short_path}/#{File.basename(file)}:"
604
+ parts = content.split(/^##+/)
605
+ parts.shift
606
+ if parts.empty?
607
+ content
608
+ else
609
+ "## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
610
+ end
611
+ end
612
+
584
613
  # Read in the build notes file and output a hash of "Title" => contents
585
614
  def read_help_file(path = nil)
586
615
  filename = path.nil? ? note_file : path
@@ -588,21 +617,7 @@ module Howzit
588
617
  help = IO.read(filename)
589
618
 
590
619
  help.gsub!(/@include\((.*?)\)/) do
591
- m = Regexp.last_match
592
- file = File.expand_path(m[1])
593
- if File.exist?(file)
594
- content = IO.read(file)
595
- home = ENV['HOME']
596
- short_path = File.dirname(file.sub(/^#{home}/, '~'))
597
- prefix = "#{short_path}:"
598
- parts = content.split(/^##+/)
599
- parts.shift
600
- content = '## ' + parts.join('## ')
601
- content.gsub!(/^(##+ *)(?=\S)/, "\\1#{prefix}")
602
- content
603
- else
604
- m[0]
605
- end
620
+ include_file(Regexp.last_match)
606
621
  end
607
622
 
608
623
  template_topics = get_template_topics(help)
@@ -673,7 +688,8 @@ module Howzit
673
688
  matches
674
689
  end
675
690
 
676
- def initialize(args)
691
+ def initialize(args = [])
692
+ Color.coloring = $stdout.isatty
677
693
  flags = {
678
694
  run: false,
679
695
  list_topics: false,
@@ -683,7 +699,8 @@ module Howzit
683
699
  title_only: false,
684
700
  choose: false,
685
701
  quiet: false,
686
- verbose: false
702
+ verbose: false,
703
+ default: false
687
704
  }
688
705
 
689
706
  defaults = {
@@ -716,7 +733,8 @@ module Howzit
716
733
  OptionParser.new do |opts|
717
734
  opts.banner = "Usage: #{__FILE__} [OPTIONS] [TOPIC]"
718
735
  opts.separator ''
719
- opts.separator 'Show build notes for the current project (buildnotes.md). Include a topic name to see just that topic, or no argument to display all.'
736
+ opts.separator 'Show build notes for the current project (buildnotes.md).
737
+ Include a topic name to see just that topic, or no argument to display all.'
720
738
  opts.separator ''
721
739
  opts.separator 'Options:'
722
740
 
@@ -725,7 +743,8 @@ module Howzit
725
743
  Process.exit 0
726
744
  end
727
745
 
728
- opts.on('-e', '--edit', "Edit buildnotes file in current working directory using #{File.basename(ENV['EDITOR'])}") do
746
+ opts.on('-e', '--edit', "Edit buildnotes file in current working directory
747
+ using $EDITOR") do
729
748
  edit_note
730
749
  Process.exit 0
731
750
  end
@@ -743,7 +762,8 @@ module Howzit
743
762
  @options[:list_topics] = true
744
763
  end
745
764
 
746
- opts.on('-m', '--matching TYPE', MATCHING_OPTIONS, 'Topics matching type', "(#{MATCHING_OPTIONS.join(', ')})") do |c|
765
+ opts.on('-m', '--matching TYPE', MATCHING_OPTIONS,
766
+ 'Topics matching type', "(#{MATCHING_OPTIONS.join(', ')})") do |c|
747
767
  @options[:matching] = c
748
768
  end
749
769
 
@@ -776,8 +796,8 @@ module Howzit
776
796
  @options[:log_level] = 0
777
797
  end
778
798
 
779
- opts.on('-u', '--upstream', 'Traverse up parent directories for additional build notes') do
780
- @options[:include_upstream] = true
799
+ opts.on('-u', '--[no-]upstream', 'Traverse up parent directories for additional build notes') do |p|
800
+ @options[:include_upstream] = p
781
801
  end
782
802
 
783
803
  opts.on('--show-code', 'Display the content of fenced run blocks') do
@@ -788,7 +808,7 @@ module Howzit
788
808
  @options[:wrap] = w.to_i
789
809
  end
790
810
 
791
- opts.on('--edit-config', "Edit configuration file using #{File.basename(ENV['EDITOR'])}") do
811
+ opts.on('--edit-config', "Edit configuration file using default $EDITOR") do
792
812
  edit_config(defaults)
793
813
  Process.exit 0
794
814
  end
@@ -802,21 +822,21 @@ module Howzit
802
822
  Dir.chdir(template_folder)
803
823
  Dir.glob('*.md').each do |file|
804
824
  template = File.basename(file, '.md')
805
- puts "\e[7;30;45mtemplate: \e[7;33;40m#{template}\e[0m"
806
- puts "\e[1;30m[\e[1;37mtasks\e[1;30m]──────────────────────────────────────┐\e[0m"
825
+ puts Color.template("{Mk}template:{Yk}#{template}{x}")
826
+ puts Color.template("{bk}[{bl}tasks{bk}]──────────────────────────────────────┐{x}")
807
827
  metadata = file.extract_metadata
808
828
  topics = read_help_file(file)
809
- topics.keys.each do |topic|
810
- puts " \e[1;30m│\e[1;37m-\e[0m \e[1;36;40m#{template}:#{topic.sub(/^.*?:/, '')}\e[0m"
829
+ topics.each_key do |topic|
830
+ puts Color.template(" {bk}│{bw}-{x} {bcK}#{template}:#{topic.sub(/^.*?:/, '')}{x}")
811
831
  end
812
832
  if metadata.size > 0
813
833
  meta = []
814
- meta << metadata['required'].split(/\s*,\s*/).map {|m| "*\e[1;37m#{m}\e[0;37m" } if metadata.key?('required')
834
+ meta << metadata['required'].split(/\s*,\s*/).map {|m| "*{bw}#{m}{xw}" } if metadata.key?('required')
815
835
  meta << metadata['optional'].split(/\s*,\s*/).map {|m| "#{m}" } if metadata.key?('optional')
816
- puts "\e[1;30m[\e[1;34mmeta\e[1;30m]───────────────────────────────────────┤\e[0m"
817
- puts " \e[1;30m│\e[1;37m \e[0;37m#{meta.join(", ")}\e[0m"
836
+ puts Color.template("{bk}[{bl}meta{bk}]───────────────────────────────────────┤{x}")
837
+ puts Color.template(" {bk}│ {xw}#{meta.join(", ")}{x}")
818
838
  end
819
- puts " \e[1;30m└───────────────────────────────────────────┘\e[0m"
839
+ puts Color.template(" {bk}└───────────────────────────────────────────┘{x}")
820
840
  end
821
841
  Process.exit 0
822
842
  end
@@ -843,9 +863,13 @@ module Howzit
843
863
  puts "Howzit v#{VERSION}"
844
864
  Process.exit 0
845
865
  end
866
+
867
+ opts.on('--default', 'Answer all prompts with default response') do
868
+ @options[:default] = true
869
+ end
846
870
  end.parse!(args)
847
871
 
848
- process(args)
872
+ @cli_args = args
849
873
  end
850
874
 
851
875
  def edit_note
@@ -862,7 +886,7 @@ module Howzit
862
886
  end
863
887
 
864
888
  ##
865
- ## @brief Traverse up directory tree looking for build notes
889
+ ## Traverse up directory tree looking for build notes
866
890
  ##
867
891
  ## @return topics dictionary
868
892
  ##
@@ -1031,7 +1055,7 @@ module Howzit
1031
1055
  File.join(config_dir, 'templates')
1032
1056
  end
1033
1057
 
1034
- def create_config
1058
+ def create_config(defaults)
1035
1059
  dir, file = [config_dir, config_file]
1036
1060
  unless File.directory?(dir)
1037
1061
  warn "Creating config directory at #{dir}"
@@ -1046,7 +1070,7 @@ module Howzit
1046
1070
  end
1047
1071
 
1048
1072
  def load_config(defaults)
1049
- file = create_config
1073
+ file = create_config(defaults)
1050
1074
  config = YAML.load(IO.read(file))
1051
1075
  newconfig = config ? defaults.merge(config) : defaults
1052
1076
  write_config(newconfig)
@@ -1064,7 +1088,7 @@ module Howzit
1064
1088
  `#{ENV['EDITOR']} "#{config_file}"`
1065
1089
  end
1066
1090
 
1067
- def process(args)
1091
+ def process
1068
1092
  output = []
1069
1093
 
1070
1094
  unless note_file
@@ -1080,13 +1104,13 @@ module Howzit
1080
1104
  end
1081
1105
 
1082
1106
  if @options[:title_only]
1083
- out = get_note_title(note_file, 20)
1107
+ out = get_note_title(20)
1084
1108
  $stdout.print(out.strip)
1085
1109
  Process.exit(0)
1086
1110
  elsif @options[:output_title]
1087
- title = get_note_title(note_file)
1111
+ title = get_note_title
1088
1112
  if title && !title.empty?
1089
- header = format_header(title, { hr: "\u{2550}", color: '1;37;40' })
1113
+ header = format_header(title, { hr: "\u{2550}", color: '{bwK}' })
1090
1114
  output.push("#{header}\n")
1091
1115
  end
1092
1116
  end
@@ -1118,14 +1142,14 @@ module Howzit
1118
1142
  elsif @options[:choose]
1119
1143
  topic_match = choose(topics.keys)
1120
1144
  # If there are arguments use those to search for a matching topic
1121
- elsif !args.empty?
1145
+ elsif !@cli_args.empty?
1122
1146
 
1123
- search = args.join(' ').strip.downcase
1147
+ search = @cli_args.join(' ').strip.downcase
1124
1148
  matches = match_topic(search)
1125
1149
 
1126
1150
  if matches.empty?
1127
- output.push(%(\e[0;31mERROR: No topic match found for \e[1;33m#{search}\e[0m\n))
1128
- if !@options[:show_all_on_error]
1151
+ output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{search}{x}\n)))
1152
+ unless @options[:show_all_on_error]
1129
1153
  show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
1130
1154
  Process.exit 1
1131
1155
  end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cribbed from <https://github.com/flori/term-ansicolor>
4
+ module Howzit
5
+ # Terminal output color functions.
6
+ module Color
7
+ ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/.freeze
8
+ # All available color names. Available as methods and string extensions.
9
+ #
10
+ # @example Use a color as a method. Color reset will be added to end of string.
11
+ # Color.yellow('This text is yellow') => "\e[33mThis text is yellow\e[0m"
12
+ #
13
+ # @example Use a color as a string extension. Color reset added automatically.
14
+ # 'This text is green'.green => "\e[1;32mThis text is green\e[0m"
15
+ #
16
+ # @example Send a text string as a color
17
+ # Color.send('red') => "\e[31m"
18
+ ATTRIBUTES = [
19
+ [:clear, 0], # String#clear is already used to empty string in Ruby 1.9
20
+ [:reset, 0], # synonym for :clear
21
+ [:bold, 1],
22
+ [:dark, 2],
23
+ [:italic, 3], # not widely implemented
24
+ [:underline, 4],
25
+ [:underscore, 4], # synonym for :underline
26
+ [:blink, 5],
27
+ [:rapid_blink, 6], # not widely implemented
28
+ [:negative, 7], # no reverse because of String#reverse
29
+ [:concealed, 8],
30
+ [:strikethrough, 9], # not widely implemented
31
+ [:strike, 9], # not widely implemented
32
+ [:black, 30],
33
+ [:red, 31],
34
+ [:green, 32],
35
+ [:yellow, 33],
36
+ [:blue, 34],
37
+ [:magenta, 35],
38
+ [:purple, 35],
39
+ [:cyan, 36],
40
+ [:white, 37],
41
+ [:bgblack, 40],
42
+ [:bgred, 41],
43
+ [:bggreen, 42],
44
+ [:bgyellow, 43],
45
+ [:bgblue, 44],
46
+ [:bgmagenta, 45],
47
+ [:bgpurple, 45],
48
+ [:bgcyan, 46],
49
+ [:bgwhite, 47],
50
+ [:boldblack, 90],
51
+ [:boldred, 91],
52
+ [:boldgreen, 92],
53
+ [:boldyellow, 93],
54
+ [:boldblue, 94],
55
+ [:boldmagenta, 95],
56
+ [:boldpurple, 95],
57
+ [:boldcyan, 96],
58
+ [:boldwhite, 97],
59
+ [:boldbgblack, 100],
60
+ [:boldbgred, 101],
61
+ [:boldbggreen, 102],
62
+ [:boldbgyellow, 103],
63
+ [:boldbgblue, 104],
64
+ [:boldbgmagenta, 105],
65
+ [:boldbgpurple, 105],
66
+ [:boldbgcyan, 106],
67
+ [:boldbgwhite, 107],
68
+ [:softpurple, '0;35;40'],
69
+ [:hotpants, '7;34;40'],
70
+ [:knightrider, '7;30;40'],
71
+ [:flamingo, '7;31;47'],
72
+ [:yeller, '1;37;43'],
73
+ [:whiteboard, '1;30;47'],
74
+ [:chalkboard, '1;37;40'],
75
+ [:led, '0;32;40'],
76
+ [:redacted, '0;30;40'],
77
+ [:alert, '1;31;43'],
78
+ [:error, '1;37;41'],
79
+ [:default, '0;39']
80
+ ].map(&:freeze).freeze
81
+
82
+ ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
83
+
84
+ # Returns true if Howzit::Color supports the +feature+.
85
+ #
86
+ # The feature :clear, that is mixing the clear color attribute into String,
87
+ # is only supported on ruby implementations, that do *not* already
88
+ # implement the String#clear method. It's better to use the reset color
89
+ # attribute instead.
90
+ def support?(feature)
91
+ case feature
92
+ when :clear
93
+ !String.instance_methods(false).map(&:to_sym).include?(:clear)
94
+ end
95
+ end
96
+
97
+ # Template coloring
98
+ class ::String
99
+ ##
100
+ ## Extract the longest valid %color name from a string.
101
+ ##
102
+ ## Allows %colors to bleed into other text and still
103
+ ## be recognized, e.g. %greensomething still finds
104
+ ## %green.
105
+ ##
106
+ ## @return [String] a valid color name
107
+ ##
108
+ def validate_color
109
+ valid_color = nil
110
+ compiled = ''
111
+ normalize_color.split('').each do |char|
112
+ compiled += char
113
+ valid_color = compiled if Color.attributes.include?(compiled.to_sym) || compiled =~ /^([fb]g?)?#([a-f0-9]{6})$/i
114
+ end
115
+
116
+ valid_color
117
+ end
118
+
119
+ ##
120
+ ## Normalize a color name, removing underscores,
121
+ ## replacing "bright" with "bold", and converting
122
+ ## bgbold to boldbg
123
+ ##
124
+ ## @return [String] Normalized color name
125
+ ##
126
+ def normalize_color
127
+ gsub(/_/, '').sub(/bright/i, 'bold').sub(/bgbold/, 'boldbg')
128
+ end
129
+
130
+ # Get the calculated ANSI color at the end of the
131
+ # string
132
+ #
133
+ # @return ANSI escape sequence to match color
134
+ #
135
+ def last_color_code
136
+ m = scan(ESCAPE_REGEX)
137
+
138
+ em = ['0']
139
+ fg = nil
140
+ bg = nil
141
+ rgbf = nil
142
+ rgbb = nil
143
+
144
+ m.each do |c|
145
+ case c
146
+ when '0'
147
+ em = ['0']
148
+ fg, bg, rgbf, rgbb = nil
149
+ when /^[34]8/
150
+ case c
151
+ when /^3/
152
+ fg = nil
153
+ rgbf = c
154
+ when /^4/
155
+ bg = nil
156
+ rgbb = c
157
+ end
158
+ else
159
+ c.split(/;/).each do |i|
160
+ x = i.to_i
161
+ if x <= 9
162
+ em << x
163
+ elsif x >= 30 && x <= 39
164
+ rgbf = nil
165
+ fg = x
166
+ elsif x >= 40 && x <= 49
167
+ rgbb = nil
168
+ bg = x
169
+ elsif x >= 90 && x <= 97
170
+ rgbf = nil
171
+ fg = x
172
+ elsif x >= 100 && x <= 107
173
+ rgbb = nil
174
+ bg = x
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ escape = "\e[#{em.join(';')}m"
181
+ escape += "\e[#{rgbb}m" if rgbb
182
+ escape += "\e[#{rgbf}m" if rgbf
183
+ escape + "\e[#{[fg, bg].delete_if(&:nil?).join(';')}m"
184
+ end
185
+ end
186
+
187
+ class << self
188
+ # Returns true if the coloring function of this module
189
+ # is switched on, false otherwise.
190
+ def coloring?
191
+ @coloring
192
+ end
193
+
194
+ attr_writer :coloring
195
+
196
+ ##
197
+ ## Enables colored output
198
+ ##
199
+ ## @example Turn color on or off based on TTY
200
+ ## Howzit::Color.coloring = STDOUT.isatty
201
+ def coloring
202
+ @coloring ||= true
203
+ end
204
+
205
+ ##
206
+ ## Convert a template string to a colored string.
207
+ ## Colors are specified with single letters inside
208
+ ## curly braces. Uppercase changes background color.
209
+ ##
210
+ ## w: white, k: black, g: green, l: blue, y: yellow, c: cyan,
211
+ ## m: magenta, r: red, b: bold, u: underline, i: italic,
212
+ ## x: reset (remove background, color, emphasis)
213
+ ##
214
+ ## @example Convert a templated string
215
+ ## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
216
+ ##
217
+ ## @param input [String, Array] The template
218
+ ## string. If this is an array, the
219
+ ## elements will be joined with a
220
+ ## space.
221
+ ##
222
+ ## @return [String] Colorized string
223
+ ##
224
+ def template(input)
225
+ input = input.join(' ') if input.is_a? Array
226
+ input.gsub!(/%/, '%%')
227
+ fmt = input.gsub(/\{(\w+)\}/) do
228
+ Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
229
+ end
230
+
231
+ colors = { w: white, k: black, g: green, l: blue,
232
+ y: yellow, c: cyan, m: magenta, r: red,
233
+ W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
234
+ Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
235
+ b: bold, u: underline, i: italic, x: reset }
236
+
237
+ format(fmt, colors)
238
+ end
239
+ end
240
+
241
+ ATTRIBUTES.each do |c, v|
242
+ new_method = <<-EOSCRIPT
243
+ def #{c}(string = nil)
244
+ result = ''
245
+ result << "\e[#{v}m" if Howzit::Color.coloring?
246
+ if block_given?
247
+ result << yield
248
+ elsif string.respond_to?(:to_str)
249
+ result << string.to_str
250
+ elsif respond_to?(:to_str)
251
+ result << to_str
252
+ else
253
+ return result #only switch on
254
+ end
255
+ result << "\e[0m" if Howzit::Color.coloring?
256
+ result
257
+ end
258
+ EOSCRIPT
259
+
260
+ module_eval(new_method)
261
+
262
+ next unless c =~ /bold/
263
+
264
+ # Accept brightwhite in addition to boldwhite
265
+ new_method = <<-EOSCRIPT
266
+ def #{c.to_s.sub(/bold/, 'bright')}(string = nil)
267
+ result = ''
268
+ result << "\e[#{v}m" if Howzit::Color.coloring?
269
+ if block_given?
270
+ result << yield
271
+ elsif string.respond_to?(:to_str)
272
+ result << string.to_str
273
+ elsif respond_to?(:to_str)
274
+ result << to_str
275
+ else
276
+ return result #only switch on
277
+ end
278
+ result << "\e[0m" if Howzit::Color.coloring?
279
+ result
280
+ end
281
+ EOSCRIPT
282
+
283
+ module_eval(new_method)
284
+ end
285
+
286
+ def rgb(hex)
287
+ is_bg = hex.match(/^bg?#/) ? true : false
288
+ hex_string = hex.sub(/^([fb]g?)?#/, '')
289
+
290
+ parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
291
+ t = []
292
+ %w[r g b].each do |e|
293
+ t << parts[e].hex
294
+ end
295
+ color =
296
+ "\e[#{is_bg ? '48' : '38'};2;#{t.join(';')}m"
297
+ end
298
+
299
+ # Regular expression that is used to scan for ANSI-sequences while
300
+ # uncoloring strings.
301
+ COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/.freeze
302
+
303
+ # Returns an uncolored version of the string, that is all
304
+ # ANSI-sequences are stripped from the string.
305
+ def uncolor(string = nil) # :yields:
306
+ if block_given?
307
+ yield.to_str.gsub(COLORED_REGEXP, '')
308
+ elsif string.respond_to?(:to_str)
309
+ string.to_str.gsub(COLORED_REGEXP, '')
310
+ elsif respond_to?(:to_str)
311
+ to_str.gsub(COLORED_REGEXP, '')
312
+ else
313
+ ''
314
+ end
315
+ end
316
+
317
+ # Returns an array of all Howzit::Color attributes as symbols.
318
+ def attributes
319
+ ATTRIBUTE_NAMES
320
+ end
321
+ extend self
322
+ end
323
+ end
data/lib/howzit/prompt.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howzit
4
+ # Command line prompt utils
4
5
  module Prompt
5
6
  def yn(prompt, default = true)
7
+ return default if !$stdout.isatty
8
+
6
9
  system 'stty cbreak'
7
10
  yn = color_single_options(default ? %w[Y n] : %w[y N])
8
11
  $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
@@ -5,7 +5,7 @@ module Howzit
5
5
  module StringUtils
6
6
  # Just strip out color codes when requested
7
7
  def uncolor
8
- gsub(/\e\[[\d;]+m/, '').gsub(/\e]1337;SetMark/,'')
8
+ gsub(/\e\[[\d;]+m/, '').gsub(/\e\]1337;SetMark/,'')
9
9
  end
10
10
 
11
11
  # Adapted from https://github.com/pazdera/word_wrap/,
@@ -2,5 +2,5 @@
2
2
  # Primary module for this gem.
3
3
  module Howzit
4
4
  # Current Howzit version.
5
- VERSION = '1.2.10'.freeze
5
+ VERSION = '1.2.13'.freeze
6
6
  end
data/lib/howzit.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'howzit/version'
2
2
  require 'howzit/prompt'
3
+ require 'howzit/colors'
3
4
  require 'howzit/buildnotes'
4
5
  require 'howzit/stringutils'
5
6
  require 'optparse'
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Howzit::BuildNotes do
4
- subject(:ruby_gem) { Howzit::BuildNotes.new }
4
+ subject(:ruby_gem) { Howzit::BuildNotes.new([]) }
5
5
 
6
6
  describe ".new" do
7
7
  it "makes a new instance" do
@@ -9,3 +9,32 @@ describe Howzit::BuildNotes do
9
9
  end
10
10
  end
11
11
  end
12
+
13
+ describe Howzit::BuildNotes do
14
+ Dir.chdir('spec')
15
+ how = Howzit::BuildNotes.new(['--no-upstream', '--default'])
16
+ how.create_note
17
+ subject { how }
18
+
19
+ describe ".note_file" do
20
+ it "locates a build note file" do
21
+ expect(subject.note_file).not_to be_empty
22
+ end
23
+ end
24
+
25
+ describe ".grep_topics" do
26
+ it "found editable" do
27
+ expect(subject.grep_topics('editable')).to include('File Structure')
28
+ expect(subject.grep_topics('editable')).not_to include('Build')
29
+ end
30
+ end
31
+
32
+ describe ".list_topic_titles" do
33
+ it "found 4 topics" do
34
+ expect(subject.topics.keys.count).to eq 4
35
+ end
36
+ it "outputs a newline-separated string" do
37
+ expect(subject.list_topic_titles.scan(/\n/).count).to eq 3
38
+ end
39
+ end
40
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,13 +1,13 @@
1
- require 'simplecov'
1
+ # require 'simplecov'
2
2
 
3
- SimpleCov.start
3
+ # SimpleCov.start
4
4
 
5
- if ENV['CI'] == 'true'
6
- require 'codecov'
7
- SimpleCov.formatter = SimpleCov::Formatter::Codecov
8
- else
9
- SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
10
- end
5
+ # if ENV['CI'] == 'true'
6
+ # require 'codecov'
7
+ # SimpleCov.formatter = SimpleCov::Formatter::Codecov
8
+ # else
9
+ # SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
10
+ # end
11
11
 
12
12
  require 'howzit'
13
13
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howzit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.10
4
+ version: 1.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-01 00:00:00.000000000 Z
11
+ date: 2022-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -150,20 +150,6 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0.9'
153
- - !ruby/object:Gem::Dependency
154
- name: codecov
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - "~>"
158
- - !ruby/object:Gem::Version
159
- version: '0.1'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - "~>"
165
- - !ruby/object:Gem::Version
166
- version: '0.1'
167
153
  - !ruby/object:Gem::Dependency
168
154
  name: fuubar
169
155
  requirement: !ruby/object:Gem::Requirement
@@ -283,6 +269,7 @@ files:
283
269
  - lib/.rubocop.yml
284
270
  - lib/howzit.rb
285
271
  - lib/howzit/buildnotes.rb
272
+ - lib/howzit/colors.rb
286
273
  - lib/howzit/prompt.rb
287
274
  - lib/howzit/stringutils.rb
288
275
  - lib/howzit/version.rb