# frozen_string_literal: true module Doing ## ## String helpers ## class ::String include Doing::Color ## ## Determines if receiver is surrounded by slashes or starts with single quote ## ## @return True if regex, False otherwise. ## def is_rx? self =~ %r{(^/.*?/$|^')} end ## ## Convert string to fuzzy regex. Characters in words ## can be separated by up to *distance* characters in ## haystack, spaces indicate unlimited distance. ## ## @example "this word".to_rx(2) => ## /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/ ## ## @param distance [Integer] Allowed distance ## between characters ## @param case_type The case type ## ## @return [Regexp] Regex pattern ## def to_rx(distance: 3, case_type: :smart) case_sensitive = case case_type when :smart self =~ /[A-Z]/ ? true : false when :sensitive true else false end pattern = case dup.strip when %r{^/.*?/$} sub(%r{/(.*?)/}, '\1') when /^'/ sub(/^'(.*?)'?$/, '\1') else split(/ +/).map { |w| w.split('').join(".{0,#{distance}}") }.join('.*?') end Regexp.new(pattern, !case_sensitive) end ## ## Test string for truthiness (0, "f", "false", "n", "no" all return false, case insensitive, otherwise true) ## ## @return [Boolean] String is truthy ## def truthy? if self =~ /^(0|f(alse)?|n(o)?)$/i false else true end end ## @param (see #highlight_tags) def highlight_tags!(color = 'yellow') replace highlight_tags(color) end ## ## Colorize @tags with ANSI escapes ## ## @param color [String] color (see #Color) ## ## @return [String] string with @tags highlighted ## def highlight_tags(color = 'yellow') escapes = scan(/(\e\[[\d;]+m)[^\e]+@/) color = color.split(' ') unless color.is_a?(Array) tag_color = '' color.each { |c| tag_color += Doing::Color.send(c) } last_color = if !escapes.empty? escapes[-1][0] else Doing::Color.default end gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{Doing::Color.reset}#{last_color}") end ## ## Test if line should be ignored ## ## @return [Boolean] line is empty or comment ## def ignore? line = self line =~ /^#/ || line =~ /^\s*$/ end ## ## Truncate to nearest word ## ## @param len The length ## def truncate(len, ellipsis: '...') return self if length <= len total = 0 res = [] split(/ /).each do |word| break if total + 1 + word.length > len total += 1 + word.length res.push(word) end res.join(' ') + ellipsis end def truncate!(len, ellipsis: '...') replace truncate(len, ellipsis: ellipsis) end ## ## Truncate string in the middle ## ## @param len The length ## @param ellipsis The ellipsis ## def truncmiddle(len, ellipsis: '...') return self if length <= len len -= (ellipsis.length / 2).to_i total = length half = total / 2 cut = (total - len) / 2 sub(/(.{#{half - cut}}).*?(.{#{half - cut}})$/, "\\1#{ellipsis}\\2") end def truncmiddle!(len, ellipsis: '...') replace truncmiddle(len, ellipsis: ellipsis) end ## ## Remove color escape codes ## ## @return clean string ## def uncolor gsub(/\e\[[\d;]+m/,'') end def uncolor! replace uncolor end ## ## Wrap string at word breaks, respecting tags ## ## @param len [Integer] The length ## @param offset [Integer] (Optional) The width to pad each subsequent line ## @param prefix [String] (Optional) A prefix to add to each line ## def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '') last_color = color.empty? ? '' : after.last_color note_rx = /(?i-m)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)/ # Don't break inside of tag values str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') } words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') } out = [] line = [] words.each do |word| if line.join(' ').uncolor.length + word.uncolor.length + 1 > len out.push(line.join(' ')) line.clear end line << word.uncolor end out.push(line.join(' ')) note = '' after.sub!(note_rx) do note = Regexp.last_match(0) '' end out[0] = format("%-#{pad}s%s%s", out[0], last_color, after) left_pad = ' ' * offset left_pad += indent out.map { |l| "#{left_pad}#{color}#{l}#{last_color}" }.join("\n").strip + last_color + " #{note}".chomp end ## ## Capitalize on the first character on string ## ## @return Capitalized string ## def cap_first sub(/^\w/) do |m| m.upcase end end ## ## Convert a sort order string to a qualified type ## ## @return [String] 'asc' or 'desc' ## def normalize_order!(default = 'asc') replace normalize_order(default) end def normalize_order(default = 'asc') case self when /^a/i 'asc' when /^d/i 'desc' else default end end ## ## Convert a case sensitivity string to a symbol ## ## @return Symbol :smart, :sensitive, :ignore ## def normalize_case! replace normalize_case end def normalize_case(default = :smart) case self when /^c/i :sensitive when /^i/i :ignore when /^s/i :smart else default.is_a?(Symbol) ? default : default.normalize_case end end ## ## Convert a boolean string to a symbol ## ## @return Symbol :and, :or, or :not ## def normalize_bool!(default = :and) replace normalize_bool(default) end def normalize_bool(default = :and) case self when /(and|all)/i :and when /(any|or)/i :or when /(not|none)/i :not else default.is_a?(Symbol) ? default : default.normalize_bool end end def normalize_trigger! replace normalize_trigger end def normalize_trigger gsub(/\((?!\?:)/, '(?:').downcase end def to_tags gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map { |t| t.strip.sub(/^@/, '') } end def add_tags!(tags, remove: false) replace add_tags(tags, remove: remove) end def add_tags(tags, remove: false) title = self.dup tags = tags.to_tags tags.each { |tag| title.tag!(tag, remove: remove) } title end def tag!(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false) replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex, single: single) end def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false) log_level = single ? :info : :debug title = dup title.chomp! tag = tag.sub(/^@?/, '') case_sensitive = tag !~ /[A-Z]/ rx_tag = if regex tag.gsub(/\./, '\S') else tag.gsub(/\?/, '.').gsub(/\*/, '\S*?') end if remove || rename_to return title unless title =~ /#{rx_tag}(?=[ (]|$)/ rx = Regexp.new("(^| )@#{rx_tag}(\\([^)]*\\))?(?= |$)", case_sensitive) if title =~ rx title.gsub!(rx) do m = Regexp.last_match rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1] end title.dedup_tags! title.chomp! if rename_to f = "@#{tag}".cyan t = "@#{rename_to}".cyan Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}")) else f = "@#{tag}".cyan Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}")) end else Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}") end elsif title =~ /@#{tag}(?=[ (]|$)/ Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}") return title else add = tag add += "(#{value})" unless value.nil? title.chomp! title += " @#{add}" title.dedup_tags! title.chomp! Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}")) end title.gsub(/ +/, ' ') end ## ## Remove duplicate tags, leaving only first occurrence ## ## @return Deduplicated string ## def dedup_tags! replace dedup_tags end def dedup_tags title = dup tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq tags.each do |tag| found = false title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m| if found '' else found = true m end end end title end # Returns the last escape sequence from a string # # @param string The string to examine # def last_color scan(/\e\[[\d;]+m/).join('') end ## ## Turn raw urls into HTML links ## ## @param opt [Hash] Additional Options ## def link_urls!(opt = {}) replace link_urls(opt) end def link_urls(opt = {}) opt[:format] ||= :html str = self.dup if :format == :markdown # Remove <self-linked> formatting str.gsub!(/<(.*?)>/) do |match| m = Regexp.last_match if m[1] =~ /^https?:/ m[1] else match end end end # Replace qualified urls str.gsub!(%r{(?mi)(?<!["'\[(\\])((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@^=%&/~+#])?}) do |_match| m = Regexp.last_match proto = m[1].nil? ? 'http://' : '' case opt[:format] when :html %(<a href="#{proto}#{m[0]}" title="Link to #{m[0].sub(/^https?:\/\//, '')}">[#{m[3]}]</a>) when :markdown "[#{m[0]}](#{proto}#{m[0]})" else m[0] end end # Clean up unlinked <urls> str.gsub!(/<(\w+:.*?)>/) do |match| m = Regexp.last_match if m[1] =~ /<a href/ match else %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>) end end str end end end