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