Module: Howzit::StringUtils
- Included in:
- String
- Defined in:
- lib/howzit/stringutils.rb
Overview
String Extensions
Instance Method Summary collapse
-
#available? ⇒ Boolean
Test if an executable is available on the system.
-
#build_note? ⇒ Boolean
Test if the filename matches the conditions to be a build note.
-
#c ⇒ String
Shortcut for calling Color.template.
-
#comp_distance(term) ⇒ Float
Compare strings and return a distance.
-
#contains_count(chars) ⇒ Object
Number of matching characters the string contains.
-
#distance(chars) ⇒ Number
Determine the minimum distance between characters that they all still fall within.
-
#extract_metadata ⇒ Hash
Split the content at the first top-level header and assume everything before it is metadata.
-
#format_header(opts = {}) ⇒ String
Make a fancy title line for the topic.
-
#in_distance?(chars, distance) ⇒ Boolean
Determine if a series of characters are all within a given distance of each other in the String.
-
#in_order(chars) ⇒ Boolean
Determine if characters are in order.
-
#iterm_marker ⇒ String
Output an iTerm marker.
-
#metadata ⇒ Hash
Examine text for metadata and return key/value pairs.
-
#normalize_metadata(meta) ⇒ Hash
Autocorrect some keys.
-
#note_title(file, truncate = 0) ⇒ Object
Get the title of the build note (top level header).
-
#preserve_escapes ⇒ String
Replace slash escaped characters in a string with a zero-width space that will prevent a shell from interpreting them when output to console.
-
#render_arguments ⇒ String
Render $X placeholders based on positional arguments.
- #render_named_placeholders ⇒ Object
- #render_numeric_placeholders ⇒ Object
-
#render_template(vars) ⇒ String
Render [%variable] placeholders in a templated string.
-
#render_template!(vars) ⇒ Object
Render [%variable] placeholders in place.
-
#should_mark_iterm? ⇒ Boolean
Test if iTerm markers should be output.
-
#split_line(width, indent = '') ⇒ Object
Splits a line at nearest word break.
-
#to_config_value(orig_value = nil) ⇒ Object
Convert a string to a valid YAML value.
-
#to_rx ⇒ Regexp
Convert a string to a regex object based on matching settings.
-
#trunc(len) ⇒ Object
Truncate string to nearest word.
-
#trunc!(len) ⇒ Object
Truncate string in place (destructive).
-
#uncolor ⇒ Object
Just strip out color codes when requested.
-
#wrap(width) ⇒ String
Wrap text at a specified width.
-
#wrap!(width) ⇒ Object
Wrap string in place (destructive).
Instance Method Details
#available? ⇒ Boolean
Test if an executable is available on the system
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
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 |
#c ⇒ String
Shortcut for calling Color.template
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
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
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
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_metadata ⇒ Hash
Split the content at the first top-level header and assume everything before it is metadata. Passes to #metadata for processing
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
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 = { hr: "\u{254C}", color: '{bg}', border: '{x}', mark: should_mark_iterm? } .merge!(opts) case Howzit.[:header_format] when :block Color.template("\n\n#{[:color]}\u{258C}#{title}#{should_mark_iterm? && [:mark] ? iterm_marker : ''}{x}") else cols = TTY::Screen.columns cols = Howzit.[:wrap] if Howzit.[:wrap].positive? && cols > Howzit.[:wrap] title = Color.template("#{[:border]}#{[:hr] * 2}( #{[:color]}#{title}#{[: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 = [:hr] * remaining tail = if should_mark_iterm? "#{hr_tail}#{[: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
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
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_marker ⇒ String
Output an iTerm marker
515 516 517 |
# File 'lib/howzit/stringutils.rb', line 515 def iterm_marker "\e]1337;SetMark\a" if should_mark_iterm? end |
#metadata ⇒ Hash
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)
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
483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/howzit/stringutils.rb', line 483 def () data = {} .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)
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_escapes ⇒ String
Replace slash escaped characters in a string with a zero-width space that will prevent a shell from interpreting them when output to console
141 142 143 |
# File 'lib/howzit/stringutils.rb', line 141 def preserve_escapes gsub(/\\([a-z])/, '\\1') end |
#render_arguments ⇒ String
Render $X placeholders based on positional arguments
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_placeholders ⇒ Object
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_placeholders ⇒ Object
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
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
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
505 506 507 |
# File 'lib/howzit/stringutils.rb', line 505 def should_mark_iterm? ENV['TERM_PROGRAM'] =~ /^iTerm/ && !Howzit.[:run] && !Howzit.[:paginate] end |
#split_line(width, indent = '') ⇒ Object
Splits a line at nearest word break
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
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_rx ⇒ Regexp
Convert a string to a regex object based on matching settings
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.[: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
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)
312 313 314 |
# File 'lib/howzit/stringutils.rb', line 312 def trunc!(len) replace trunc(len) end |
#uncolor ⇒ Object
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
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)
291 292 293 |
# File 'lib/howzit/stringutils.rb', line 291 def wrap!(width) replace(wrap(width)) end |