Skip to content

Commit

Permalink
Merge pull request #579 from ProGM/implement-custom-filters
Browse files Browse the repository at this point in the history
Adding additional configurable filters to tags
  • Loading branch information
seuros committed Aug 28, 2014
2 parents ada4cbd + 0608e41 commit 35be549
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 74 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ As such, a _Feature_ would map to either major or minor. A _bug fix_ to a patch.

* Breaking Changes
* Features
* [@ProGM Support for custom parsers for tags](https://github.com/mbleigh/acts-as-taggable-on/pull/579)

* Fixes
* Performance
* Misc
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,43 @@ to allow for dynamic tag contexts (this could be user generated tag contexts!)
User.tagged_with("same", :on => :customs) # => [@user]
```

### Tag Parsers

If you want to change how tags are parsed, you can define a your own implementation:

```ruby
class MyParser < ActsAsTaggableOn::GenericParser
def parse
TagList.new.tap do |tag_list|
tag_list.add @tag_list.split('|')
end
end
end
```

Now you can use this parser, passing it as parameter:

```ruby
@user = User.new(:name => "Bobby")
@user.tag_list = "east, south"
@user.tag_list.add("north|west", parser: MyParser)
@user.tag_list # => ["north", "east", "south", "west"]

# Or also:
@user.tag_list.parser = MyParser
@user.tag_list.add("north|west")
@user.tag_list # => ["north", "east", "south", "west"]
```

Or change it globally:

```ruby
ActsAsTaggable.default_parser = MyParser
@user = User.new(:name => "Bobby")
@user.tag_list = "east|south"
@user.tag_list # => ["east", "south"]
```

### Tag Ownership

Tags can have owners:
Expand Down
14 changes: 13 additions & 1 deletion lib/acts-as-taggable-on.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module ActsAsTaggableOn

autoload :Tag
autoload :TagList
autoload :GenericParser
autoload :DefaultParser
autoload :TagListParser
autoload :Taggable
autoload :Tagger
Expand Down Expand Up @@ -57,14 +59,24 @@ def self.glue

class Configuration
attr_accessor :delimiter, :force_lowercase, :force_parameterize,
:strict_case_match, :remove_unused_tags
:strict_case_match, :remove_unused_tags, :default_parser

def initialize
@delimiter = ','
@force_lowercase = false
@force_parameterize = false
@strict_case_match = false
@remove_unused_tags = false
@default_parser = DefaultParser
end

def delimiter=(string)
ActiveRecord::Base.logger.warn <<WARNING
ActsAsTaggableOn.delimiter is deprecated \
and will be removed from v4.0+, use \
a ActsAsTaggableOn.default_parser instead
WARNING
@delimiter = string
end
end

Expand Down
79 changes: 79 additions & 0 deletions lib/acts_as_taggable_on/default_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module ActsAsTaggableOn
##
# Returns a new TagList using the given tag string.
#
# Example:
# tag_list = ActsAsTaggableOn::DefaultParser.parse("One , Two, Three")
# tag_list # ["One", "Two", "Three"]
class DefaultParser < GenericParser

def parse
string = @tag_list

string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
TagList.new.tap do |tag_list|
string = string.to_s.dup

string.gsub!(double_quote_pattern) {
# Append the matched tag to the tag list
tag_list << Regexp.last_match[2]
# Return the matched delimiter ($3) to replace the matched items
''
}

string.gsub!(single_quote_pattern) {
# Append the matched tag ($2) to the tag list
tag_list << Regexp.last_match[2]
# Return an empty string to replace the matched items
''
}

# split the string by the delimiter
# and add to the tag_list
tag_list.add(string.split(Regexp.new delimiter))
end
end


# private
def delimiter
# Parse the quoted tags
d = ActsAsTaggableOn.delimiter
# Separate multiple delimiters by bitwise operator
d = d.join('|') if d.kind_of?(Array)
d
end

# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*" # quote (") optionally preceded by whitespace
# (.*?) # Tag ($2)
# "\s* # quote (") optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def double_quote_pattern
/(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
end

# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*' # quote (') optionally preceded by whitespace
# (.*?) # Tag ($2)
# '\s* # quote (') optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def single_quote_pattern
/(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
end

end

end
19 changes: 19 additions & 0 deletions lib/acts_as_taggable_on/generic_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module ActsAsTaggableOn
##
# Returns a new TagList using the given tag string.
#
# Example:
# tag_list = ActsAsTaggableOn::GenericParser.new.parse("One , Two, Three")
# tag_list # ["One", "Two", "Three"]
class GenericParser
def initialize(tag_list)
@tag_list = tag_list
end

def parse
TagList.new.tap do |tag_list|
tag_list.add @tag_list.split(',').map(&:strip).reject(&:empty?)
end
end
end
end
12 changes: 8 additions & 4 deletions lib/acts_as_taggable_on/tag_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
module ActsAsTaggableOn
class TagList < Array
attr_accessor :owner
attr_accessor :parser

def initialize(*args)
@parser = ActsAsTaggableOn.default_parser
add(*args)
end

Expand Down Expand Up @@ -88,9 +90,11 @@ def clean!

def extract_and_apply_options!(args)
options = args.last.is_a?(Hash) ? args.pop : {}
options.assert_valid_keys :parse
options.assert_valid_keys :parse, :parser

args.map! { |a| TagListParser.parse(a) } if options[:parse]
parser = options[:parser] ? options[:parser] : @parser

args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]

args.flatten!
end
Expand All @@ -101,9 +105,9 @@ def self.from(string)
ActiveRecord::Base.logger.warn <<WARNING
ActsAsTaggableOn::TagList.from is deprecated \
and will be removed from v4.0+, use \
ActsAsTaggableOn::TagListParser.parse instead
ActsAsTaggableOn::DefaultParser.new instead
WARNING
TagListParser.parse(string)
@parser.new(string).parse
end


Expand Down
71 changes: 7 additions & 64 deletions lib/acts_as_taggable_on/tag_list_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,15 @@ module ActsAsTaggableOn
# tag_list # ["One", "Two", "Three"]
module TagListParser
class << self
## DEPRECATED
def parse(string)
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
TagList.new.tap do |tag_list|
string = string.to_s.dup


string.gsub!(double_quote_pattern) {
# Append the matched tag to the tag list
tag_list << Regexp.last_match[2]
# Return the matched delimiter ($3) to replace the matched items
''
}

string.gsub!(single_quote_pattern) {
# Append the matched tag ($2) to the tag list
tag_list << Regexp.last_match[2]
# Return an empty string to replace the matched items
''
}

# split the string by the delimiter
# and add to the tag_list
tag_list.add(string.split(Regexp.new delimiter))
end
ActiveRecord::Base.logger.warn <<WARNING
ActsAsTaggableOn::TagListParser.parse is deprecated \
and will be removed from v4.0+, use \
ActsAsTaggableOn::TagListParser.new instead
WARNING
DefaultParser.new(string).parse
end


# private
def delimiter
# Parse the quoted tags
d = ActsAsTaggableOn.delimiter
# Separate multiple delimiters by bitwise operator
d = d.join('|') if d.kind_of?(Array)
d
end

# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*" # quote (") optionally preceded by whitespace
# (.*?) # Tag ($2)
# "\s* # quote (") optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def double_quote_pattern
/(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
end

# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*' # quote (') optionally preceded by whitespace
# (.*?) # Tag ($2)
# '\s* # quote (') optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def single_quote_pattern
/(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
end

end
end
end
8 changes: 4 additions & 4 deletions lib/acts_as_taggable_on/taggable/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def grouped_column_names_for(object)
# User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
# User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
def tagged_with(tags, options = {})
tag_list = ActsAsTaggableOn::TagListParser.parse(tags)
tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
options = options.dup
empty_result = where('1 = 0')

Expand Down Expand Up @@ -278,7 +278,7 @@ def tag_list_cache_on(context)
if instance_variable_get(variable_name)
instance_variable_get(variable_name)
elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
instance_variable_set(variable_name, ActsAsTaggableOn::TagListParser.parse(cached_tag_list_on(context)))
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
else
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
end
Expand Down Expand Up @@ -328,7 +328,7 @@ def set_tag_list_on(context, new_list)
variable_name = "@#{context.to_s.singularize}_list"
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)

instance_variable_set(variable_name, ActsAsTaggableOn::TagListParser.parse(new_list))
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
end

def tagging_contexts
Expand All @@ -348,7 +348,7 @@ def process_dirty_object(context, new_list)
if self.class.preserve_tag_order
changed_attributes[attrib] = old if old.to_s != value.to_s
else
changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn::TagListParser.parse(value).sort
changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/acts_as_taggable_on/taggable/ownership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def set_owner_tag_list_on(owner, context, new_list)

cache = cached_owned_tag_list_on(context)

cache[owner] = ActsAsTaggableOn::TagListParser.parse(new_list)
cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse
end

def reload(*args)
Expand Down
47 changes: 47 additions & 0 deletions spec/acts_as_taggable_on/default_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- encoding : utf-8 -*-
require 'spec_helper'

describe ActsAsTaggableOn::DefaultParser do
it '#parse should return empty array if empty array is passed' do
parser = ActsAsTaggableOn::DefaultParser.new([])
expect(parser.parse).to be_empty
end

describe 'Multiple Delimiter' do
before do
@old_delimiter = ActsAsTaggableOn.delimiter
end

after do
ActsAsTaggableOn.delimiter = @old_delimiter
end

it 'should separate tags by delimiters' do
ActsAsTaggableOn.delimiter = [',', ' ', '\|']
parser = ActsAsTaggableOn::DefaultParser.new('cool, data|I have')
expect(parser.parse.to_s).to eq('cool, data, I, have')
end

it 'should escape quote' do
ActsAsTaggableOn.delimiter = [',', ' ', '\|']
parser = ActsAsTaggableOn::DefaultParser.new("'I have'|cool, data")
expect(parser.parse.to_s).to eq('"I have", cool, data')

parser = ActsAsTaggableOn::DefaultParser.new('"I, have"|cool, data')
expect(parser.parse.to_s).to eq('"I, have", cool, data')
end

it 'should work for utf8 delimiter and long delimiter' do
ActsAsTaggableOn.delimiter = [',', '的', '可能是']
parser = ActsAsTaggableOn::DefaultParser.new('我的东西可能是不见了,还好有备份')
expect(parser.parse.to_s).to eq('我, 东西, 不见了, 还好有备份')
end

it 'should work for multiple quoted tags' do
ActsAsTaggableOn.delimiter = [',']
parser = ActsAsTaggableOn::DefaultParser.new('"Ruby Monsters","eat Katzenzungen"')
expect(parser.parse.to_s).to eq('Ruby Monsters, eat Katzenzungen')
end
end

end
Loading

0 comments on commit 35be549

Please sign in to comment.