Class: String
- Inherits:
-
Object
- Object
- String
- Defined in:
- lib/na/colors.rb,
lib/na/string.rb
Overview
String helpers
Instance Method Summary collapse
-
#action ⇒ String
Returns the action text with leading dash and whitespace removed.
- #action? ⇒ Boolean
- #blank? ⇒ Boolean
-
#cap_first ⇒ String
Capitalize first character, leaving other capitalization in place.
-
#cap_first! ⇒ String
Capitalize the first character of the string in place.
-
#chronify(**options) ⇒ DateTime
Converts input string into a Time object when input takes on the following formats: - interval format e.g.
-
#comment(char = '#') ⇒ Object
Insert a comment character at the start of every line.
-
#dir_matches?(any: [], all: [], none: [], require_last: true, distance: 1) ⇒ Boolean
Check if the string matches directory patterns using any, all, and none criteria.
-
#dir_to_rx(distance: 1, require_last: true) ⇒ Object
Convert a directory path to a regular expression.
- #done? ⇒ Boolean
-
#expand_date_tags(additional_tags = nil) ⇒ Object
Convert (chronify) natural language dates within configured date tags (tags whose value is expected to be a date).
-
#good? ⇒ Boolean
Tests if object is nil or empty.
-
#highlight_filename ⇒ String
Colorize the dirname and filename of a path.
-
#highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action]) ⇒ Object
Highlight search results.
-
#highlight_tags(color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens], last_color: NA.theme[:action]) ⇒ String
Colorize @tags with ANSI escapes.
-
#ignore? ⇒ Boolean
Test if line should be ignored.
-
#indent_level ⇒ Number
Determine indentation level of line.
-
#last_color ⇒ String
Returns the last escape sequence from a string.
-
#last_color_code ⇒ Object
Get the calculated ANSI color at the end of the string.
-
#matches?(any: [], all: [], none: []) ⇒ Boolean
Check if the string matches any, all, and none regex patterns.
- #na? ⇒ Boolean
-
#normalize_color ⇒ String
Normalize a color name, removing underscores, replacing “bright” with “bold”, and converting bgbold to boldbg.
-
#project ⇒ String?
Returns the project name if matched, otherwise nil.
- #project? ⇒ Boolean
-
#read_file ⇒ String
Returns the contents of the file, or raises if missing.
-
#shorten_path ⇒ String
Replace home directory with tilde.
-
#trunc_middle(max) ⇒ String
Truncate the string in the middle, replacing the removed section with ‘[…]’.
-
#validate_color ⇒ String
Extract the longest valid %color name from a string.
-
#wildcard_to_rx ⇒ String
Convert wildcard characters to regular expressions.
-
#wrap(width, indent) ⇒ String
Wrap the string to a given width, indenting each line and preserving tag formatting.
Instance Method Details
#action ⇒ String
Returns the action text with leading dash and whitespace removed.
104 105 106 |
# File 'lib/na/string.rb', line 104 def action sub(/^[ \t]*- /, '') end |
#action? ⇒ Boolean
83 84 85 |
# File 'lib/na/string.rb', line 83 def action? self =~ /^[ \t]*- / end |
#blank? ⇒ Boolean
87 88 89 |
# File 'lib/na/string.rb', line 87 def blank? strip =~ /^$/ end |
#cap_first ⇒ String
Capitalize first character, leaving other capitalization in place
271 272 273 274 275 276 |
# File 'lib/na/string.rb', line 271 def cap_first sub(/^([a-z])(.*)$/) do m = Regexp.last_match m[1].upcase << m[2] end end |
#cap_first! ⇒ String
Capitalize the first character of the string in place.
265 266 267 |
# File 'lib/na/string.rb', line 265 def cap_first! replace cap_first end |
#chronify(**options) ⇒ DateTime
Converts input string into a Time object when input takes on the following formats:
- interval format e.g. '1d2h30m', '45m' etc.
- a semantic phrase e.g. 'yesterday 5:30pm'
- a strftime e.g. '2016-03-15 15:32:04 PDT'
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/na/string.rb', line 336 def chronify(**) now = Time.now raise StandardError, "Invalid time expression #{inspect}" if to_s.strip == '' secs_ago = if match(/^(\d+)$/) # plain number, assume minutes Regexp.last_match(1).to_i * 60 elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i)) # day/hour/minute format e.g. 1d2h30m [[m['day'], 24 * 3600], [m['hour'], 3600], [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+) end if secs_ago res = now - secs_ago notify(%(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)), debug: true) else date_string = dup date_string = 'today' if date_string.match(REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i date_string = "#{options[:context]} #{date_string}" if date_string =~ REGEX_TIME && [:context] require 'chronic' unless defined?(Chronic) res = Chronic.parse(date_string, { guess: .fetch(:guess, :begin), context: .fetch(:future, false) ? :future : :past, ambiguous_time_range: 8 }) NA.notify(%(date/time string "#{self}" interpreted as #{res}), debug: true) end res end |
#comment(char = '#') ⇒ Object
Insert a comment character at the start of every line
35 36 37 |
# File 'lib/na/string.rb', line 35 def comment(char = '#') split("\n").map { |l| "#{char} #{l}" }.join("\n") end |
#dir_matches?(any: [], all: [], none: [], require_last: true, distance: 1) ⇒ Boolean
Check if the string matches directory patterns using any, all, and none criteria.
241 242 243 244 245 246 |
# File 'lib/na/string.rb', line 241 def dir_matches?(any: [], all: [], none: [], require_last: true, distance: 1) any_rx = any.map { |q| q.dir_to_rx(distance: distance, require_last: require_last) } all_rx = all.map { |q| q.dir_to_rx(distance: distance, require_last: require_last) } none_rx = none.map { |q| q.dir_to_rx(distance: distance, require_last: false) } matches_any?(any_rx) && matches_all?(all_rx) && matches_none?(none_rx) end |
#dir_to_rx(distance: 1, require_last: true) ⇒ Object
Splits at / or :, adds variable distance between characters, joins segments with slashes and requires that last segment match last segment of target path
Convert a directory path to a regular expression
228 229 230 231 232 |
# File 'lib/na/string.rb', line 228 def dir_to_rx(distance: 1, require_last: true) "#{split(%r{[/:]}).map do |comp| comp.chars.join(".{0,#{distance}}").gsub('*', '[^ ]*?') end.join('.*?/.*?')}#{'[^/]*?$' if require_last}" end |
#done? ⇒ Boolean
108 109 110 |
# File 'lib/na/string.rb', line 108 def done? self =~ /@done/ end |
#expand_date_tags(additional_tags = nil) ⇒ Object
Convert (chronify) natural language dates within configured date tags (tags whose value is expected to be a date). Modifies string in place.
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/na/string.rb', line 286 def ( = nil) iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/ = [ 'due', 'start(?:ed)?', 'beg[ia]n', 'done', 'finished', 'completed?', 'waiting', 'defer(?:red)?' ] if = = .split(/ *, */) if .is_a?(String) .map! do |tag| tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip end .concat().uniq! end done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i dup.gsub(done_rx) do m = Regexp.last_match t = m['tag'] d = m['date'] # Determine whether to bias toward future or past parsing # Non-done tags usually bias to future, except explicit past phrases like "ago", "yesterday", or "last ..." explicit_past = d =~ /(\bago\b|yesterday|\blast\b)/i future = if t =~ /^(done|complete)/ false else explicit_past ? false : true end parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future) parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})" end end |
#good? ⇒ Boolean
Tests if object is nil or empty
41 42 43 |
# File 'lib/na/string.rb', line 41 def good? !strip.empty? end |
#highlight_filename ⇒ String
Colorize the dirname and filename of a path
118 119 120 121 122 123 124 |
# File 'lib/na/string.rb', line 118 def highlight_filename return '' if nil? dir = File.dirname(self).shorten_path.trunc_middle(TTY::Screen.columns / 3) file = NA.include_ext ? File.basename(self) : File.basename(self, ".#{NA.extension}") "#{NA.theme[:dirname]}#{dir}/#{NA.theme[:filename]}#{file}{x}" end |
#highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action]) ⇒ Object
Highlight search results
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/na/string.rb', line 151 def highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action]) # Skip if string already contains ANSI codes - applying regex to colored text # will break escape sequences (e.g., searching for "3" will match "3" in "38;2;236;204;135m") return self if include?("\e") # Original simple approach for strings without ANSI codes string = dup color = NA::Color.template(color.dup) regexes.each do |rx| next if rx.nil? rx = Regexp.new(rx, Regexp::IGNORECASE) if rx.is_a?(String) string.gsub!(rx) do m = Regexp.last_match last = m.pre_match.last_color "#{color}#{m[0]}#{NA::Color.template(last)}" end end string end |
#highlight_tags(color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens], last_color: NA.theme[:action]) ⇒ String
Colorize @tags with ANSI escapes
132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/na/string.rb', line 132 def (color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens], last_color: NA.theme[:action]) tag_color = NA::Color.template(color) paren_color = NA::Color.template(parens) value_color = NA::Color.template(value) gsub(/(?<pre>\s|m)(?<tag>@[^ ("']+)(?:(?<lparen>\()(?<val>.*?)(?<rparen>\)))?/) do m = Regexp.last_match if m['val'] "#{m['pre']}#{tag_color}#{m['tag']}#{paren_color}(#{value_color}#{m['val']}#{paren_color})#{last_color}" else "#{m['pre']}#{tag_color}#{m['tag']}#{last_color}" end end end |
#ignore? ⇒ Boolean
Test if line should be ignored
47 48 49 50 |
# File 'lib/na/string.rb', line 47 def ignore? line = self line =~ /^#/ || line.strip.empty? end |
#indent_level ⇒ Number
Determine indentation level of line
76 77 78 79 80 81 |
# File 'lib/na/string.rb', line 76 def indent_level prefix = match(/(^[ \t]+)/) return 0 if prefix.nil? prefix[1].gsub(' ', "\t").scan("\t").count end |
#last_color ⇒ String
Actually returns all escape codes, with the assumption that the result of inserting them will generate the same color as was set at end of the string. Because you can send modifiers like dark and bold separate from color codes, only using the last code may not render the same style.
Returns the last escape sequence from a string.
220 221 222 |
# File 'lib/na/string.rb', line 220 def last_color scan(/\e\[[\d;]+m/).join.gsub("\e[0m", '') end |
#last_color_code ⇒ Object
Get the calculated ANSI color at the end of the string
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/na/colors.rb', line 139 def last_color_code m = scan(ESCAPE_REGEX) em = ['0'] fg = nil bg = nil rgbf = nil rgbb = nil m.each do |c| case c when '0' em = ['0'] fg, bg, rgbf, rgbb = nil when /^[34]8/ case c when /^3/ fg = nil rgbf = c when /^4/ bg = nil rgbb = c end else c.split(';').each do |i| x = i.to_i if x <= 9 em << x elsif x.between?(30, 39) rgbf = nil fg = x elsif x.between?(40, 49) rgbb = nil bg = x elsif x.between?(90, 97) rgbf = nil fg = x elsif x.between?(100, 107) rgbb = nil bg = x end end end end escape = "\e[#{em.join(';')}m" escape += "\e[#{rgbb}m" if rgbb escape += "\e[#{rgbf}m" if rgbf escape + "\e[#{[fg, bg].delete_if(&:nil?).join(';')}m" end |
#matches?(any: [], all: [], none: []) ⇒ Boolean
Check if the string matches any, all, and none regex patterns.
253 254 255 |
# File 'lib/na/string.rb', line 253 def matches?(any: [], all: [], none: []) matches_any?(any) && matches_all?(all) && matches_none?(none) end |
#na? ⇒ Boolean
112 113 114 |
# File 'lib/na/string.rb', line 112 def na? self =~ /@#{NA.na_tag}\b/ end |
#normalize_color ⇒ String
Normalize a color name, removing underscores, replacing “bright” with “bold”, and converting bgbold to boldbg
130 131 132 |
# File 'lib/na/colors.rb', line 130 def normalize_color gsub('_', '').sub(/bright/i, 'bold').sub('bgbold', 'boldbg') end |
#project ⇒ String?
Returns the project name if matched, otherwise nil.
97 98 99 100 |
# File 'lib/na/string.rb', line 97 def project m = match(/^([ \t]*)([^-][^@:]*?): *(@\S+ *)*$/) m ? m[2] : nil end |
#project? ⇒ Boolean
91 92 93 |
# File 'lib/na/string.rb', line 91 def project? !action? && self =~ /:( +@\S+(\([^)]*\))?)*$/ end |
#read_file ⇒ String
Returns the contents of the file, or raises if missing. Handles directories and NA extension.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/na/string.rb', line 56 def read_file file = File.(self) raise "Missing file #{file}" unless File.exist?(file) if File.directory?(file) if File.exist?("#{file}.#{NA.extension}") file = "#{file}.#{NA.extension}" elsif File.exist?("#{file}/#{File.basename(file)}.#{NA.extension}") file = "#{file}/#{File.basename(file)}.#{NA.extension}" else NA.notify("#{NA.theme[:error]}#{file} is a directory", exit_code: 2) end end # IO.read(file).force_encoding('ASCII-8BIT').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') File.read(file).force_encoding('utf-8') end |
#shorten_path ⇒ String
Replace home directory with tilde
280 281 282 |
# File 'lib/na/string.rb', line 280 def shorten_path sub(/^#{Dir.home}/, '~') end |
#trunc_middle(max) ⇒ String
Truncate the string in the middle, replacing the removed section with ‘[…]’.
175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/na/string.rb', line 175 def trunc_middle(max) return '' if nil? return self unless length > max half = (max / 2).floor - 3 cs = chars pre = cs.slice(0, half) post = cs.reverse.slice(0, half).reverse "#{pre.join}[...]#{post.join}" end |
#validate_color ⇒ String
Extract the longest valid %color name from a string.
Allows %colors to bleed into other text and still be recognized, e.g. %greensomething still finds %green.
112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/na/colors.rb', line 112 def validate_color valid_color = nil compiled = '' normalize_color.chars.each do |char| compiled += char if Color.attributes.include?(compiled.to_sym) || compiled =~ /^([fb]g?)?#([a-f0-9]{6})$/i valid_color = compiled end end valid_color end |
#wildcard_to_rx ⇒ String
Convert wildcard characters to regular expressions
259 260 261 |
# File 'lib/na/string.rb', line 259 def wildcard_to_rx gsub('.', '\\.').gsub('?', '.').gsub('*', '[^ ]*?') end |
#wrap(width, indent) ⇒ String
Wrap the string to a given width, indenting each line and preserving tag formatting.
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 |
# File 'lib/na/string.rb', line 191 def wrap(width, indent) return to_s if width.nil? || width <= 0 output = [] line = [] # Track visible length of current line (exclude the separating space before first word) length = -1 text = gsub(/(@\S+)\((.*?)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(2).gsub(/ /, '†')})" } text.split.each do |word| uncolored = NA::Color.uncolor(word) candidate = length + 1 + uncolored.length if candidate <= width line << word length = candidate else output << line.join(' ') line = [word] length = uncolored.length end end output << line.join(' ') # Indent all lines after the first output.each_with_index.map { |l, i| i.zero? ? l : (' ' * indent) + l }.join("\n").gsub(/†/, ' ') end |