module Tagtical class TagList < Array class TagValue < String attr_accessor :relevance cattr_accessor :relevance_delimiter self.relevance_delimiter = ':' def initialize(value="", relevance=nil) @relevance = relevance.to_f if relevance super(value) end def self.parse(input) new(*input.to_s.split(relevance_delimiter, 2).each(&:strip!)) end end cattr_accessor :delimiter self.delimiter = ',' cattr_accessor :value_quotes self.value_quotes = ["'", "\""] attr_accessor :owner def initialize(*args) add(*args) unless args.empty? end ## # Returns a new TagList using the given tag string. # # Example: # tag_list = TagList.from("One , Two, Three") # tag_list # ["One", "Two", "Three"] <=== as TagValue def self.from(*args) new(*args) end def concat(values) super(values.map! { |v| convert_tag_value(v) }) end def push(value) super(convert_tag_value(value)) end alias << push ## # Add tags to the tag_list. Duplicate or blank tags will be ignored. # Use the :parse option to add an unparsed tag string. # # Example: # tag_list.add("Fun", "Happy") # tag_list.add("Fun, Happy") # tag_list.add("Fun" => "0.546", "Happy" => 0.465) # add relevance def add(*values) extract_and_apply_options!(values) clean!(values) do concat(values) end self end ## # Remove specific tags from the tag_list. # Use the :parse option to add an unparsed tag string. # # Example: # tag_list.remove("Sad", "Lonely") # tag_list.remove("Sad, Lonely") def remove(*values) extract_and_apply_options!(values) delete_if { |value| values.include?(value) } self end ## # Transform the tag_list into a tag string suitable for edting in a form. # The tags are joined with TagList.delimiter and quoted if necessary. # # Example: # tag_list = TagList.new("Round", "Square,Cube") # tag_list.to_s # 'Round, "Square,Cube"' def to_s tag_list = frozen? ? self.dup : self tag_list.send(:clean!) tag_list.map do |tag_value| value = tag_value.include?(delimiter) ? %{"#{tag_value}"} : tag_value [value, tag_value.relevance].compact.join(TagValue.relevance_delimiter) end.join(delimiter.gsub(/(\S)$/, '\1 ')) end # Builds an option statement for an ActiveRecord table. def to_sql_conditions(options={}) options.reverse_merge!(:class => Tagtical::Tag, :column => "value", :operator => "=") "(" + map { |t| options[:class].send(:sanitize_sql, ["#{options[:class].table_name}.#{options[:column]} #{options[:operator]} ?", t]) }.join(" OR ") + ")" end private # Remove whitespace, duplicates, and blanks. def clean!(values=nil) delete_if { |value| values.include?(value) } if values.present? # Allow editing of relevance yield if block_given? reject!(&:blank?) each(&:strip!) uniq!(&:downcase) end def extract_and_apply_options!(args) options = args.last.is_a?(Hash) && args.size > 1 ? args.pop : {} options.assert_valid_keys :parse args.map! { |a| extract(a, options) } args.flatten! end def extract(input, options={}) case input when String if !input.include?(delimiter) || options[:parse]==false [input] else input, arr = input.dup, [] # Parse the quoted tags value_quotes.each do |value_quote| input.gsub!(/(\A|#{delimiter})\s*#{value_quote}(.*?)#{value_quote}\s*(#{delimiter}\s*|\z)/) { arr << $2 ; $3 } end # Parse the unquoted tags arr.concat(input.split(delimiter).each(&:strip!)) end when Hash input.map { |value, relevance| TagValue.new(value, relevance) } when Array input end end def convert_tag_value(value) value.is_a?(TagValue) ? value : TagValue.parse(value) end end end