# frozen_string_literal: true module Doing # Handling of @tags in strings class ::String ## ## Add @ prefix to string if needed, maintains +/- prefix ## ## @return [String] @string ## def add_at strip.sub(/^([+-]*)@?/, '\1@') end ## ## Removes @ prefix if needed, maintains +/- prefix ## ## @return [String] string without @ prefix ## def remove_at strip.sub(/^([+-]*)@?/, '\1') end ## ## Convert a list of tags to an array. Tags can be with ## or without @ symbols, separated by any character, and ## can include parenthetical values (with spaces) ## ## @return [Array] array of tags including @ symbols ## def to_tags arr = gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at) if block_given? yield arr else arr end end ## ## @brief Adds tags to a string ## ## @param tags [String or Array] List of tags to add. @ symbol optional ## @param remove [Boolean] remove tags instead of adding ## ## @return [String] the tagged string ## def add_tags(tags, remove: false) title = dup tags = tags.to_tags tags.each { |tag| title.tag!(tag, remove: remove) } title end ## @see #add_tags def add_tags!(tags, remove: false) replace add_tags(tags, remove: remove) end ## ## Add, rename, or remove a tag in place ## ## @see #tag ## def tag!(tag, **options) replace tag(tag, **options) end ## ## Add, rename, or remove a tag ## ## @param tag The tag ## @param value [String] Value for tag (@tag(value)) ## @param remove [Boolean] Remove the tag instead of adding ## @param rename_to [String] Replace tag with this tag ## @param regex [Boolean] Tag is regular expression ## @param single [Boolean] Operating on a single item (for logging) ## @param force [Boolean] With rename_to, add tag if it doesn't exist ## ## @return [String] The string with modified tags ## def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: 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 rx = Regexp.new("(?<=^| )@#{rx_tag}(?\\((?[^)]*)\\))?(?= |$)", case_sensitive) m = title.match(rx) if m.nil? && rename_to && force title.tag!(rename_to, value: value, single: single) elsif m title.gsub!(rx) do rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : '' 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 title = dup tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq tags.each do |tag| found = false title.gsub!(/( |^)#{Regexp.escape(tag[1])}(\([^)]+\))?(?= |$)/) do |m| if found '' else found = true m end end end title end ## @see #dedup_tags def dedup_tags! replace dedup_tags end end end