diff --git a/lib/diaspora/taggable.rb b/lib/diaspora/taggable.rb
index 0f0d43df8..388fc56d7 100644
--- a/lib/diaspora/taggable.rb
+++ b/lib/diaspora/taggable.rb
@@ -4,8 +4,6 @@
module Diaspora
module Taggable
- VALID_TAG_BODY = /[^_,\s#*\[\]()\@\/"'\.%]+\b/
-
def self.included(model)
model.class_eval do
cattr_accessor :field_with_tags
@@ -27,10 +25,11 @@ module Diaspora
end
def tag_strings
- regex = /(?:^|\s)#(#{VALID_TAG_BODY})/
- matches = self.send(self.class.field_with_tags).scan(regex).map do |match|
- match.last
- end
+ regex = /(?:^|\s)#([\w-]+|<3)/
+ matches = self.
+ send( self.class.field_with_tags ).
+ scan(regex).
+ map { |match| match[0] }
unique_matches = matches.inject(Hash.new) do |h,element|
h[element.downcase] = element unless h[element.downcase]
h
@@ -39,13 +38,20 @@ module Diaspora
end
def self.format_tags(text, opts={})
- return text if opts[:plain_text]
+ return text if opts[:plain_text]
+
text = ERB::Util.h(text) unless opts[:no_escape]
- regex = /(^|\s|>)#(#{VALID_TAG_BODY})/
- form_message = text.to_str.gsub(regex) do |matched_string|
- "#{$~[1]}##{$~[2]}"
- end
- form_message.html_safe
+ regex = /(^|\s|>)#([\w-]+|<3)/
+
+ text.to_str.gsub(regex) { |matched_string|
+ pre, url_bit, clickable = $1, $2, "##{$2}"
+ if $2 == '<3'
+ # Special case for love, because the world needs more love.
+ url_bit = '<3'
+ end
+
+ %{#{pre}#{clickable}}
+ }.html_safe
end
end
end
diff --git a/spec/shared_behaviors/taggable.rb b/spec/shared_behaviors/taggable.rb
index 2eb2d95c3..cec1e60fa 100644
--- a/spec/shared_behaviors/taggable.rb
+++ b/spec/shared_behaviors/taggable.rb
@@ -12,6 +12,10 @@ describe Diaspora::Taggable do
def controller
end
+ def tag_link(s)
+ link_to "##{s}", "/tags/#{s}", :class => 'tag'
+ end
+
describe '.format_tags' do
before do
@str = '#what #hey #vöglein'
@@ -19,14 +23,63 @@ describe Diaspora::Taggable do
@object.build_tags
@object.save!
end
+
it 'links the tag to /p' do
- link = link_to('#vöglein', '/tags/vöglein', :class => 'tag')
- Diaspora::Taggable.format_tags(@str).should include(link)
+ Diaspora::Taggable.format_tags(@str).should include( tag_link('vöglein') )
end
+
it 'responds to plain_text' do
Diaspora::Taggable.format_tags(@str, :plain_text => true).should == @str
end
+
+ it "doesn't mangle text when tags are involved" do
+ expected = {
+ nil => '',
+ '' => '',
+ 'abc' => 'abc',
+ 'a #b c' => "a #{tag_link('b')} c",
+ '#' => '#',
+ '##' => '##',
+ '###' => '###',
+ '#a' => tag_link('a'),
+ '#foobar' => tag_link('foobar'),
+ '#foocar
' => "#{tag_link('foocar')}<br>",
+ '#fooo@oo' => "#{tag_link('fooo')}@oo",
+ '#num3ric hash tags' => "#{tag_link('num3ric')} hash tags",
+ '#12345 tag' => "#{tag_link('12345')} tag",
+ '#12cde tag' => "#{tag_link('12cde')} tag",
+ '#abc45 tag' => "#{tag_link('abc45')} tag",
+ '#<3' => %{#<3},
+ 'i #<3' => %{i #<3},
+ 'i #<3 you' => %{i #<3 you},
+ '#<4' => '#<4',
+ 'test#foo test' => 'test#foo test',
+ 'test.#joo bar' => 'test.#joo bar',
+ 'test #foodar test' => "test #{tag_link('foodar')} test",
+ 'test #foofar
test' => "test #{tag_link('foofar')}<br> test",
+ 'test #gooo@oo test' => "test #{tag_link('gooo')}@oo test",
+ 'test #foo-test test' => "test #{tag_link('foo-test')} test",
+ 'test #hoo' => "test #{tag_link('hoo')}",
+ 'test #two_word tags' => "test #{tag_link('two_word')} tags",
+ 'test #three_word_tags' => "test #{tag_link('three_word_tags')}",
+ '#terminal_underscore_' => tag_link('terminal_underscore_'),
+ '#terminalunderscore_' => tag_link('terminalunderscore_'),
+ '#_initialunderscore' => tag_link('_initialunderscore'),
+ '#_initial_underscore' => tag_link('_initial_underscore'),
+ '#terminalhyphen-' => tag_link('terminalhyphen-'),
+ '#terminal-hyphen-' => tag_link('terminal-hyphen-'),
+ '#terminalhyphen- tag' => "#{tag_link('terminalhyphen-')} tag",
+ '#-initialhyphen' => tag_link('-initialhyphen'),
+ '#-initialhyphen tag' => "#{tag_link('-initialhyphen')} tag",
+ '#-initial-hyphen' => tag_link('-initial-hyphen'),
+ }
+
+ expected.each do |input,output|
+ Diaspora::Taggable.format_tags(input).should == output
+ end
+ end
end
+
describe '#build_tags' do
it 'builds the tags' do
@object.send(@object.class.field_with_tags_setter, '#what')
@@ -37,14 +90,60 @@ describe Diaspora::Taggable do
}.should change{@object.tags.count}.by(1)
end
end
+
describe '#tag_strings' do
it 'returns a string for every #thing' do
str = '#what #hey #that"smybike. #@hey ##boo # #THATWASMYBIKE #vöglein #hey#there #135440we #abc/23 ### #h!gh #ok? #see: #re:publica'
- arr = ['what', 'hey', 'that', 'THATWASMYBIKE', 'vöglein', '135440we', 'abc', 'h!gh', 'ok', 'see', 're:publica']
+ arr = ['what', 'hey', 'that', 'THATWASMYBIKE', 'vöglein', '135440we', 'abc', 'h', 'ok', 'see', 're']
@object.send(@object.class.field_with_tags_setter, str)
@object.tag_strings.should =~ arr
end
+
+ it 'extracts tags despite surrounding text' do
+ expected = {
+ '' => nil,
+ '#' => nil,
+ '##' => nil,
+ '###' => nil,
+ '#a' => 'a',
+ '#foobar' => 'foobar',
+ '#foocar
' => 'foocar',
+ '#fooo@oo' => 'fooo',
+ '#num3ric hash tags' => 'num3ric',
+ '#12345 tag' => '12345',
+ '#12cde tag' => '12cde',
+ '#abc45 tag' => 'abc45',
+ '#<3' => '<3',
+ '#<4' => nil,
+ 'test#foo test' => nil,
+ 'test.#joo bar' => nil,
+ 'test #foodar test' => 'foodar',
+ 'test #foofar
test' => 'foofar',
+ 'test #gooo@oo test' => 'gooo',
+ 'test #<3 test' => '<3',
+ 'test #foo-test test' => 'foo-test',
+ 'test #hoo' => 'hoo',
+ 'test #two_word tags' => 'two_word',
+ 'test #three_word_tags' => 'three_word_tags',
+ '#terminal_underscore_' => 'terminal_underscore_',
+ '#terminalunderscore_' => 'terminalunderscore_',
+ '#_initialunderscore' => '_initialunderscore',
+ '#_initial_underscore' => '_initial_underscore',
+ '#terminalhyphen-' => 'terminalhyphen-',
+ '#terminal-hyphen-' => 'terminal-hyphen-',
+ '#terminalhyphen- tag' => 'terminalhyphen-',
+ '#-initialhyphen' => '-initialhyphen',
+ '#-initialhyphen tag' => '-initialhyphen',
+ '#-initial-hyphen' => '-initial-hyphen',
+ }
+
+ expected.each do |text,hashtag|
+ @object.send @object.class.field_with_tags_setter, text
+ @object.tag_strings.should == [hashtag].compact
+ end
+ end
+
it 'returns no duplicates' do
str = '#what #what #what #whaaaaaaaaaat'
arr = ['what','whaaaaaaaaaat']
@@ -52,6 +151,7 @@ describe Diaspora::Taggable do
@object.send(@object.class.field_with_tags_setter, str)
@object.tag_strings.should =~ arr
end
+
it 'is case insensitive' do
str = '#what #wHaT #WHAT'
arr = ['what']