Module: Howzit::StringUtils

Included in:
String
Defined in:
lib/howzit/stringutils.rb

Overview

String Extensions

Instance Method Summary collapse

Instance Method Details

#available?Boolean

Test if an executable is available on the system

Returns:

  • (Boolean)

    executable is available



344
345
346
# File 'lib/howzit/stringutils.rb', line 344

def available?
  Util.valid_command?(self)
end

#build_note?Boolean

Test if the filename matches the conditions to be a build note

Returns:

  • (Boolean)

    true if filename passes test



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/howzit/stringutils.rb', line 91

def build_note?
  return false if downcase !~ /^(howzit[^.]*|build[^.]+)/

  # Avoid recursion: only check ignore patterns if config is fully initialized
  # and we're not in the middle of loading ignore patterns or initializing
  begin
    # Check if config exists without triggering initialization
    return true unless Howzit.instance_variable_defined?(:@config)

    config = Howzit.instance_variable_get(:@config)
    return true unless config

    # Check if config is initializing or loading ignore patterns to prevent recursion
    return true if config.instance_variable_defined?(:@initializing) && config.instance_variable_get(:@initializing)
    if config.instance_variable_defined?(:@loading_ignore_patterns) && config.instance_variable_get(:@loading_ignore_patterns)
      return true
    end

    return false if config.respond_to?(:should_ignore) && config.should_ignore(self)
  rescue StandardError
    # If config access fails for any reason, skip the ignore check
    # This prevents recursion and handles initialization edge cases
  end

  true
end

#cString

Shortcut for calling Color.template

Returns:

  • (String)

    colorized string



181
182
183
# File 'lib/howzit/stringutils.rb', line 181

def c
  Color.template(self)
end

#comp_distance(term) ⇒ Float

Compare strings and return a distance

Parameters:

  • other (String)

    The string to compare

  • term (String)

    The search term

Returns:

  • (Float)

    distance



12
13
14
15
# File 'lib/howzit/stringutils.rb', line 12

def comp_distance(term)
  chars = term.split(//)
  contains_count(chars) + distance(chars)
end

#contains_count(chars) ⇒ Object

Number of matching characters the string contains

Parameters:

  • chars (String|Array)

    The characters



22
23
24
25
26
27
# File 'lib/howzit/stringutils.rb', line 22

def contains_count(chars)
  chars = chars.split(//) if chars.is_a?(String)
  count = 0
  chars.each { |char| count += 1 if self =~ /#{char}/i }
  count
end

#distance(chars) ⇒ Number

Determine the minimum distance between characters that they all still fall within

Parameters:

  • chars (Array)

    The characters

Returns:

  • (Number)

    distance



73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/howzit/stringutils.rb', line 73

def distance(chars)
  distance = 0
  max = length - chars.length
  return max unless in_order(chars) == chars.length

  while distance < max
    return distance if in_distance?(chars, distance)

    distance += 1
  end
  distance
end

#extract_metadataHash

Split the content at the first top-level header and assume everything before it is metadata. Passes to #metadata for processing

Returns:

  • (Hash)

    key/value pairs



419
420
421
422
423
424
425
426
# File 'lib/howzit/stringutils.rb', line 419

def 
  if File.exist?(self)
    leader = Util.read_file(self).split(/^#/)[0].strip
    leader.length.positive? ? leader. : {}
  else
    {}
  end
end

#format_header(opts = {}) ⇒ String

Make a fancy title line for the topic

Parameters:

  • opts (Hash) (defaults to: {})

    options

Returns:

  • (String)

    formatted string



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/howzit/stringutils.rb', line 525

def format_header(opts = {})
  title = dup
  options = {
    hr: "\u{254C}",
    color: '{bg}',
    border: '{x}',
    mark: should_mark_iterm?
  }

  options.merge!(opts)

  case Howzit.options[:header_format]
  when :block
    Color.template("\n\n#{options[:color]}\u{258C}#{title}#{should_mark_iterm? && options[:mark] ? iterm_marker : ''}{x}")
  else
    cols = TTY::Screen.columns

    cols = Howzit.options[:wrap] if Howzit.options[:wrap].positive? && cols > Howzit.options[:wrap]
    title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")

    # Calculate remaining width for horizontal rule, ensuring it is never negative
    remaining = cols - title.uncolor.length
    if should_mark_iterm?
      # Reserve some space for the iTerm mark escape sequence in the visual layout
      remaining -= 15
    end
    remaining = 0 if remaining.negative?

    hr_tail = options[:hr] * remaining
    tail = if should_mark_iterm?
             "#{hr_tail}#{options[:mark] ? iterm_marker : ''}"
           else
             hr_tail
           end

    Color.template("\n\n#{title}#{tail}{x}\n\n")
  end
end

#in_distance?(chars, distance) ⇒ Boolean

Determine if a series of characters are all within a given distance of each other in the String

Parameters:

  • chars (String|Array)

    The characters

  • distance (Number)

    The distance

Returns:

  • (Boolean)

    true if within distance



59
60
61
62
63
# File 'lib/howzit/stringutils.rb', line 59

def in_distance?(chars, distance)
  chars = chars.split(//) if chars.is_a?(String)
  rx = Regexp.new(chars.join(".{,#{distance}}"), 'i')
  self =~ rx ? true : false
end

#in_order(chars) ⇒ Boolean

Determine if characters are in order

Parameters:

  • chars (String|Array)

    The characters

Returns:

  • (Boolean)

    characters are in order



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/howzit/stringutils.rb', line 36

def in_order(chars)
  chars = chars.split(//) if chars.is_a?(String)
  position = 0
  in_order = 0
  chars.each do |char|
    new_pos = self[position..] =~ /#{char}/i
    if new_pos
      position += new_pos
      in_order += 1
    end
  end
  in_order
end

#iterm_markerString

Output an iTerm marker

Returns:

  • (String)

    ANSI escape sequence for iTerm marker



515
516
517
# File 'lib/howzit/stringutils.rb', line 515

def iterm_marker
  "\e]1337;SetMark\a" if should_mark_iterm?
end

#metadataHash

Examine text for metadata and return key/value pairs

Supports:

  • YAML front matter (starting with — and ending with — or …)

  • MultiMarkdown-style key: value lines (up to first blank line)

Returns:

  • (Hash)

    The metadata as key/value pairs



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/howzit/stringutils.rb', line 437

def 
  data = {}
  lines = to_s.lines
  first_idx = lines.index { |l| l !~ /^\s*$/ }
  return {} unless first_idx

  first = lines[first_idx]

  if first =~ /^---\s*$/
    # YAML front matter: between first --- and closing --- or ...
    closing_rel = lines[(first_idx + 1)..].index { |l| l =~ /^(---|\.\.\.)\s*$/ }
    closing_idx = closing_rel ? first_idx + 1 + closing_rel : lines.length
    yaml_body = lines[(first_idx + 1)...closing_idx].join
    raw = yaml_body.strip.empty? ? {} : YAML.load(yaml_body) || {}
    if raw.is_a?(Hash)
      raw.each do |k, v|
        data[k.to_s.downcase] = v
      end
    end
  else
    # MultiMarkdown-style: key: value lines up to first blank line
    header_lines = []
    lines[first_idx..].each do |l|
      break if l =~ /^\s*$/

      header_lines << l
    end
    header = header_lines.join
    header.scan(/(?mi)^(\S[\s\S]+?): ([\s\S]*?)(?=\n\S[\s\S]*?:|\Z)/).each do |m|
      data[m[0].strip.downcase] = m[1]
    end
  end

  out = (data)
  Howzit.named_arguments ||= {}
  Howzit.named_arguments = out.merge(Howzit.named_arguments)
  out
end

#normalize_metadata(meta) ⇒ Hash

Autocorrect some keys

Parameters:

  • meta (Hash)

    The metadata

Returns:

  • (Hash)

    corrected metadata



483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/howzit/stringutils.rb', line 483

def (meta)
  data = {}
  meta.each do |k, v|
    case k
    when /^te?m?pl(ate)?s?$/
      data['template'] = v
    when /^req\w*$/
      data['required'] = v
    else
      data[k] = v
    end
  end
  data
end

#note_title(file, truncate = 0) ⇒ Object

Get the title of the build note (top level header)

Parameters:

  • truncate (Integer) (defaults to: 0)

    Truncate to width



123
124
125
126
127
128
129
130
131
132
# File 'lib/howzit/stringutils.rb', line 123

def note_title(file, truncate = 0)
  title = match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
  title = if title
            title[1].nil? ? title[2] : title[1]
          else
            file.sub(/(\.\w+)?$/, '')
          end

  title && truncate.positive? ? title.trunc(truncate) : title
end

#preserve_escapesString

Replace slash escaped characters in a string with a zero-width space that will prevent a shell from interpreting them when output to console

Returns:



141
142
143
# File 'lib/howzit/stringutils.rb', line 141

def preserve_escapes
  gsub(/\\([a-z])/, '\​\1')
end

#render_argumentsString

Render $X placeholders based on positional arguments

Returns:

  • (String)

    rendered string



382
383
384
385
386
387
# File 'lib/howzit/stringutils.rb', line 382

def render_arguments
  str = dup
  str.render_named_placeholders
  str.render_numeric_placeholders
  Howzit.arguments.nil? ? str : str.gsub(/\$[@*]/, Shellwords.join(Howzit.arguments))
end

#render_named_placeholdersObject



389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/howzit/stringutils.rb', line 389

def render_named_placeholders
  gsub!(/\$\{(?<name>[A-Z0-9_]+(?::.*?)?)\}/i) do
    m = Regexp.last_match
    arg, default = m['name'].split(/:/).map(&:strip)
    if Howzit.named_arguments&.key?(arg) && !Howzit.named_arguments[arg].nil?
      Howzit.named_arguments[arg]
    elsif default
      default
    else
      # Preserve the original ${VAR} syntax if variable is not defined and no default provided
      m[0]
    end
  end
end

#render_numeric_placeholdersObject



404
405
406
407
408
409
410
# File 'lib/howzit/stringutils.rb', line 404

def render_numeric_placeholders
  gsub!(/\$\{?(\d+)\}?/) do
    arg, default = Regexp.last_match(1).split(/:/)
    idx = arg.to_i - 1
    Howzit.arguments.length > idx ? Howzit.arguments[idx] : default || Regexp.last_match(0)
  end
end

#render_template(vars) ⇒ String

Render [%variable] placeholders in a templated string

Parameters:

  • vars (Hash)

    Key/value pairs of variable values

Returns:

  • (String)

    Rendered string



356
357
358
359
360
361
362
363
364
365
366
# File 'lib/howzit/stringutils.rb', line 356

def render_template(vars)
  vars.each do |k, v|
    gsub!(/\[%#{k}(:.*?)?\]/, v)
  end

  # Replace empty variables with default
  gsub!(/\[%([^\]]+?):(.*?)\]/, '\2')

  # Remove remaining empty variables
  gsub(/\[%.*?\]/, '')
end

#render_template!(vars) ⇒ Object

Render [%variable] placeholders in place

Parameters:

  • vars (Hash)

    Key/value pairs of variable values



373
374
375
# File 'lib/howzit/stringutils.rb', line 373

def render_template!(vars)
  replace render_template(vars)
end

#should_mark_iterm?Boolean

Test if iTerm markers should be output. Requires that the $TERM_PROGRAM be iTerm and howzit is not running directives or paginating output

Returns:

  • (Boolean)

    should mark?



505
506
507
# File 'lib/howzit/stringutils.rb', line 505

def should_mark_iterm?
  ENV['TERM_PROGRAM'] =~ /^iTerm/ && !Howzit.options[:run] && !Howzit.options[:paginate]
end

#split_line(width, indent = '') ⇒ Object

Splits a line at nearest word break

Parameters:

  • width (Integer)

    The width of the first segment

  • indent (String) (defaults to: '')

    The indent string



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/howzit/stringutils.rb', line 322

def split_line(width, indent = '')
  line = dup
  at = line.index(/\s/)
  last_at = at

  while !at.nil? && at < width
    last_at = at
    at = line.index(/\s/, last_at + 1)
  end

  if last_at.nil?
    [indent + line[0, width], line[width, line.length]]
  else
    [indent + line[0, last_at], line[last_at + 1, line.length]]
  end
end

#to_config_value(orig_value = nil) ⇒ Object

Convert a string to a valid YAML value

Parameters:

  • orig_value (defaults to: nil)

    The original value from which type will be determined

Returns:

  • coerced value



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/howzit/stringutils.rb', line 152

def to_config_value(orig_value = nil)
  if orig_value
    case orig_value.class.to_s
    when /Integer/
      to_i
    when /(True|False)Class/
      self =~ /^(t(rue)?|y(es)?|1)$/i ? true : false
    else
      self
    end
  else
    case self
    when /^[0-9]+$/
      to_i
    when /^(t(rue)?|y(es)?)$/i
      true
    when /^(f(alse)?|n(o)?)$/i
      false
    else
      self
    end
  end
end

#to_rxRegexp

Convert a string to a regex object based on matching settings

Returns:

  • (Regexp)

    Receive regex representation of the object.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/howzit/stringutils.rb', line 190

def to_rx
  case Howzit.options[:matching]
  when 'exact'
    /^#{self}$/i
  when 'beginswith'
    /^#{self}/i
  when 'fuzzy'
    # For fuzzy matching, use token-based matching where each word gets fuzzy character matching
    # This allows "lst tst" to match "List Available Tests" by applying fuzzy matching to each token
    words = split(/\s+/).reject(&:empty?)
    if words.length > 1
      # Multiple words: apply character-by-character fuzzy matching to each word token
      # Then allow flexible matching between words
      pattern = words.map do |w|
        # Apply fuzzy matching to each word (character-by-character with up to 3 chars between)
        w.split(//).map { |c| Regexp.escape(c) }.join('.{0,3}?')
      end.join('.*')
      /#{pattern}/i
    else
      # Single word: character-by-character fuzzy matching for flexibility
      /#{split(//).join('.{0,3}?')}/i
    end
  when 'token'
    # Token-based matching: match words in order with any text between them
    # "list tests" matches "list available tests", "list of tests", etc.
    words = split(/\s+/).reject(&:empty?)
    if words.length > 1
      pattern = words.map { |w| Regexp.escape(w) }.join('.*')
      /#{pattern}/i
    else
      /#{Regexp.escape(self)}/i
    end
  else
    # Default 'partial' mode: token-based matching for multi-word searches
    # This allows "list tests" to match "list available tests"
    words = split(/\s+/).reject(&:empty?)
    if words.length > 1
      # Token-based: match words in order with any text between
      pattern = words.map { |w| Regexp.escape(w) }.join('.*')
      /#{pattern}/i
    else
      # Single word: simple substring match
      /#{Regexp.escape(self)}/i
    end
  end
end

#trunc(len) ⇒ Object

Truncate string to nearest word

Parameters:

  • len (Integer)

    max length of string



299
300
301
302
303
304
305
# File 'lib/howzit/stringutils.rb', line 299

def trunc(len)
  split(/ /).each_with_object([]) do |x, ob|
    break ob unless ob.join(' ').length + ' '.length + x.length <= len

    ob.push(x)
  end.join(' ').strip
end

#trunc!(len) ⇒ Object

Truncate string in place (destructive)

Parameters:

  • len (Integer)

    The length to truncate at



312
313
314
# File 'lib/howzit/stringutils.rb', line 312

def trunc!(len)
  replace trunc(len)
end

#uncolorObject

Just strip out color codes when requested



238
239
240
241
242
# File 'lib/howzit/stringutils.rb', line 238

def uncolor
  # force UTF-8 and remove invalid characters, then remove color codes
  # and iTerm markers
  gsub(Howzit::Color::COLORED_REGEXP, '').gsub(/\e\]1337;SetMark/, '')
end

#wrap(width) ⇒ String

Wrap text at a specified width.

Adapted from github.com/pazdera/word_wrap/, copyright © 2014, 2015 Radek Pazdera Distributed under the MIT License

Parameters:

  • width (Integer)

    The width at which to wrap lines

Returns:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/howzit/stringutils.rb', line 255

def wrap(width)
  width ||= 80
  output = []
  indent = ''

  text = gsub(/\t/, '  ')

  text.lines do |line|
    line.chomp! "\n"
    if line.length > width
      indent = if line.uncolor =~ /^(\s*(?:[+\-*]|\d+\.) )/
                 ' ' * Regexp.last_match[1].length
               else
                 ''
               end
      new_lines = line.split_line(width)

      while new_lines.length > 1 && new_lines[1].length + indent.length > width
        output.push new_lines[0]

        new_lines = new_lines[1].split_line(width, indent)
      end
      output += [new_lines[0], indent + new_lines[1]]
    else
      output.push line
    end
  end
  output.map!(&:rstrip)
  output.join("\n")
end

#wrap!(width) ⇒ Object

Wrap string in place (destructive)

Parameters:

  • width (Integer)

    The width at which to wrap



291
292
293
# File 'lib/howzit/stringutils.rb', line 291

def wrap!(width)
  replace(wrap(width))
end