twitter-text 1.4.17 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  require 'test/unit'
2
2
  require 'yaml'
3
+ require 'nokogiri'
3
4
 
4
5
  # Ruby 1.8 encoding check
5
6
  major, minor, patch = RUBY_VERSION.split('.')
@@ -7,7 +8,7 @@ if major.to_i == 1 && minor.to_i < 9
7
8
  $KCODE='u'
8
9
  end
9
10
 
10
- require File.expand_path(File.dirname(__FILE__) + '/../lib/twitter-text')
11
+ require File.expand_path('../../lib/twitter-text', __FILE__)
11
12
 
12
13
  class ConformanceTest < Test::Unit::TestCase
13
14
  include Twitter::Extractor
@@ -15,168 +16,166 @@ class ConformanceTest < Test::Unit::TestCase
15
16
  include Twitter::HitHighlighter
16
17
  include Twitter::Validation
17
18
 
18
- def setup
19
- @conformance_dir = ENV['CONFORMANCE_DIR'] || File.join(File.dirname(__FILE__), 'twitter-text-conformance')
20
- end
19
+ private
21
20
 
22
- module ExtractorConformance
23
- def test_replies_extractor_conformance
24
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :replies) do |description, expected, input|
25
- assert_equal expected, extract_reply_screen_name(input), description
26
- end
21
+ %w(description expected text json hits).each do |key|
22
+ define_method key.to_sym do
23
+ @test_info[key]
27
24
  end
25
+ end
28
26
 
29
- def test_mentions_extractor_conformance
30
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :mentions) do |description, expected, input|
31
- assert_equal expected, extract_mentioned_screen_names(input), description
32
- end
27
+ def assert_equal_without_attribute_order(expected, actual, failure_message = nil)
28
+ assert_block(build_message(failure_message, "<?> expected but was\n<?>", expected, actual)) do
29
+ equal_nodes?(Nokogiri::HTML(expected).root, Nokogiri::HTML(actual).root)
33
30
  end
31
+ end
34
32
 
35
- def test_mentions_with_indices_extractor_conformance
36
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :mentions_with_indices) do |description, expected, input|
37
- expected = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
38
- assert_equal expected, extract_mentioned_screen_names_with_indices(input), description
39
- end
40
- end
33
+ def equal_nodes?(expected, actual)
34
+ return false unless expected.name == actual.name
35
+ return false unless ordered_attributes(expected) == ordered_attributes(actual)
36
+ return false if expected.text? && actual.text? && !(expected.content= actual.content)
41
37
 
42
- def test_mentions_or_lists_with_indices_conformance
43
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :mentions_or_lists_with_indices) do |description, expected, input|
44
- expected = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
45
- assert_equal expected, extract_mentions_or_lists_with_indices(input), description
46
- end
38
+ expected.children.each_with_index do |child, index|
39
+ return false unless equal_nodes?(child, actual.children[index])
47
40
  end
48
41
 
49
- def test_url_extractor_conformance
50
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :urls) do |description, expected, input|
51
- assert_equal expected, extract_urls(input), description
52
- expected.each do |expected_url|
53
- assert_equal true, valid_url?(expected_url, true, false), "expected url [#{expected_url}] not valid"
54
- end
55
- end
56
- end
42
+ true
43
+ end
57
44
 
58
- def test_urls_with_indices_extractor_conformance
59
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :urls_with_indices) do |description, expected, input|
60
- expected = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
61
- assert_equal expected, extract_urls_with_indices(input), description
62
- end
63
- end
45
+ def ordered_attributes(element)
46
+ element.attribute_nodes.map{|attr| [attr.name, attr.value]}.sort
47
+ end
64
48
 
65
- def test_hashtag_extractor_conformance
66
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :hashtags) do |description, expected, input|
67
- assert_equal expected, extract_hashtags(input), description
68
- end
69
- end
49
+ CONFORMANCE_DIR = ENV['CONFORMANCE_DIR'] || File.expand_path("../twitter-text-conformance", __FILE__)
50
+
51
+ def self.def_conformance_test(file, test_type, &block)
52
+ yaml = YAML.load_file(File.join(CONFORMANCE_DIR, file))
53
+ raise "No such test suite: #{test_type.to_s}" unless yaml["tests"][test_type.to_s]
70
54
 
71
- def test_hashtags_with_indices_extractor_conformance
72
- run_conformance_test(File.join(@conformance_dir, 'extract.yml'), :hashtags_with_indices) do |description, expected, input|
73
- expected = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
74
- assert_equal expected, extract_hashtags_with_indices(input), description
55
+ yaml["tests"][test_type.to_s].each do |test_info|
56
+ name = :"test_#{test_type}_#{test_info['description']}"
57
+ define_method name do
58
+ @test_info = test_info
59
+ instance_eval(&block)
75
60
  end
76
61
  end
77
62
  end
78
- include ExtractorConformance
79
63
 
80
- module AutolinkConformance
81
- def test_users_autolink_conformance
82
- run_conformance_test(File.join(@conformance_dir, 'autolink.yml'), :usernames) do |description, expected, input|
83
- assert_equal expected, auto_link_usernames_or_lists(input, :suppress_no_follow => true), description
84
- end
85
- end
64
+ public
86
65
 
87
- def test_lists_autolink_conformance
88
- run_conformance_test(File.join(@conformance_dir, 'autolink.yml'), :lists) do |description, expected, input|
89
- assert_equal expected, auto_link_usernames_or_lists(input, :suppress_no_follow => true), description
90
- end
91
- end
66
+ # Extractor Conformance
67
+ def_conformance_test("extract.yml", :replies) do
68
+ assert_equal expected, extract_reply_screen_name(text), description
69
+ end
92
70
 
93
- def test_urls_autolink_conformance
94
- run_conformance_test(File.join(@conformance_dir, 'autolink.yml'), :urls) do |description, expected, input|
95
- assert_equal expected, auto_link_urls_custom(input, :suppress_no_follow => true), description
96
- end
97
- end
71
+ def_conformance_test("extract.yml", :mentions) do
72
+ assert_equal expected, extract_mentioned_screen_names(text), description
73
+ end
98
74
 
99
- def test_hashtags_autolink_conformance
100
- run_conformance_test(File.join(@conformance_dir, 'autolink.yml'), :hashtags) do |description, expected, input|
101
- assert_equal expected, auto_link_hashtags(input, :suppress_no_follow => true), description
102
- end
103
- end
75
+ def_conformance_test("extract.yml", :mentions_with_indices) do
76
+ e = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
77
+ assert_equal e, extract_mentioned_screen_names_with_indices(text), description
78
+ end
104
79
 
105
- def test_all_autolink_conformance
106
- run_conformance_test(File.join(@conformance_dir, 'autolink.yml'), :all) do |description, expected, input|
107
- assert_equal expected, auto_link(input, :suppress_no_follow => true), description
108
- end
80
+ def_conformance_test("extract.yml", :mentions_or_lists_with_indices) do
81
+ e = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
82
+ assert_equal e, extract_mentions_or_lists_with_indices(text), description
83
+ end
84
+
85
+ def_conformance_test("extract.yml", :urls) do
86
+ assert_equal expected, extract_urls(text), description
87
+ expected.each do |expected_url|
88
+ assert_equal true, valid_url?(expected_url, true, false), "expected url [#{expected_url}] not valid"
109
89
  end
110
90
  end
111
- include AutolinkConformance
112
91
 
113
- module HitHighlighterConformance
92
+ def_conformance_test("extract.yml", :urls_with_indices) do
93
+ e = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
94
+ assert_equal e, extract_urls_with_indices(text), description
95
+ end
114
96
 
115
- def test_plain_text_conformance
116
- run_conformance_test(File.join(@conformance_dir, 'hit_highlighting.yml'), :plain_text, true) do |config|
117
- assert_equal config['expected'], hit_highlight(config['text'], config['hits']), config['description']
118
- end
119
- end
97
+ def_conformance_test("extract.yml", :hashtags) do
98
+ assert_equal expected, extract_hashtags(text), description
99
+ end
120
100
 
121
- def test_with_links_conformance
122
- run_conformance_test(File.join(@conformance_dir, 'hit_highlighting.yml'), :with_links, true) do |config|
123
- assert_equal config['expected'], hit_highlight(config['text'], config['hits']), config['description']
124
- end
125
- end
101
+ def_conformance_test("extract.yml", :hashtags_with_indices) do
102
+ e = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
103
+ assert_equal e, extract_hashtags_with_indices(text), description
126
104
  end
127
- include HitHighlighterConformance
128
105
 
129
- module ValidationConformance
130
- def test_tweet_validation_conformance
131
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :tweets) do |description, expected, input|
132
- assert_equal expected, valid_tweet_text?(input), description
133
- end
134
- end
106
+ def_conformance_test("extract.yml", :cashtags) do
107
+ assert_equal expected, extract_cashtags(text), description
108
+ end
135
109
 
136
- def test_users_validation_conformance
137
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :usernames) do |description, expected, input|
138
- assert_equal expected, valid_username?(input), description
139
- end
140
- end
110
+ def_conformance_test("extract.yml", :cashtags_with_indices) do
111
+ e = expected.map{|elem| elem.inject({}){|h, (k,v)| h[k.to_sym] = v; h} }
112
+ assert_equal e, extract_cashtags_with_indices(text), description
113
+ end
141
114
 
142
- def test_lists_validation_conformance
143
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :lists) do |description, expected, input|
144
- assert_equal expected, valid_list?(input), description
145
- end
146
- end
115
+ # Autolink Conformance
116
+ def_conformance_test("autolink.yml", :usernames) do
117
+ assert_equal_without_attribute_order expected, auto_link_usernames_or_lists(text, :suppress_no_follow => true), description
118
+ end
147
119
 
148
- def test_urls_validation_conformance
149
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :urls) do |description, expected, input|
150
- assert_equal expected, valid_url?(input), description
151
- end
152
- end
120
+ def_conformance_test("autolink.yml", :lists) do
121
+ assert_equal_without_attribute_order expected, auto_link_usernames_or_lists(text, :suppress_no_follow => true), description
122
+ end
153
123
 
154
- def test_urls_without_protocol_validation_conformance
155
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :urls_without_protocol) do |description, expected, input|
156
- assert_equal expected, valid_url?(input, true, false), description
157
- end
158
- end
124
+ def_conformance_test("autolink.yml", :urls) do
125
+ assert_equal_without_attribute_order expected, auto_link_urls(text, :suppress_no_follow => true), description
126
+ end
159
127
 
160
- def test_hashtags_validation_conformance
161
- run_conformance_test(File.join(@conformance_dir, 'validate.yml'), :hashtags) do |description, expected, input|
162
- assert_equal expected, valid_hashtag?(input), description
163
- end
164
- end
128
+ def_conformance_test("autolink.yml", :hashtags) do
129
+ assert_equal_without_attribute_order expected, auto_link_hashtags(text, :suppress_no_follow => true), description
165
130
  end
166
- include ValidationConformance
167
131
 
168
- private
132
+ def_conformance_test("autolink.yml", :cashtags) do
133
+ assert_equal_without_attribute_order expected, auto_link_cashtags(text, :suppress_no_follow => true), description
134
+ end
169
135
 
170
- def run_conformance_test(file, test_type, hash_config = false, &block)
171
- yaml = YAML.load_file(file)
172
- assert yaml["tests"][test_type.to_s], "No such test suite: #{test_type.to_s}"
136
+ def_conformance_test("autolink.yml", :all) do
137
+ assert_equal_without_attribute_order expected, auto_link(text, :suppress_no_follow => true), description
138
+ end
173
139
 
174
- yaml["tests"][test_type.to_s].each do |test_info|
175
- if hash_config
176
- yield test_info
177
- else
178
- yield test_info['description'], test_info['expected'], test_info['text']
179
- end
180
- end
140
+ def_conformance_test("autolink.yml", :json) do
141
+ assert_equal_without_attribute_order expected, auto_link_with_json(text, ActiveSupport::JSON.decode(json), :suppress_no_follow => true), description
142
+ end
143
+
144
+ # HitHighlighter Conformance
145
+ def_conformance_test("hit_highlighting.yml", :plain_text) do
146
+ assert_equal expected, hit_highlight(text, hits), description
147
+ end
148
+
149
+ def_conformance_test("hit_highlighting.yml", :with_links) do
150
+ assert_equal expected, hit_highlight(text, hits), description
151
+ end
152
+
153
+ # Validation Conformance
154
+ def_conformance_test("validate.yml", :tweets) do
155
+ assert_equal expected, valid_tweet_text?(text), description
156
+ end
157
+
158
+ def_conformance_test("validate.yml", :usernames) do
159
+ assert_equal expected, valid_username?(text), description
160
+ end
161
+
162
+ def_conformance_test("validate.yml", :lists) do
163
+ assert_equal expected, valid_list?(text), description
164
+ end
165
+
166
+ def_conformance_test("validate.yml", :urls) do
167
+ assert_equal expected, valid_url?(text), description
168
+ end
169
+
170
+ def_conformance_test("validate.yml", :urls_without_protocol) do
171
+ assert_equal expected, valid_url?(text, true, false), description
172
+ end
173
+
174
+ def_conformance_test("validate.yml", :hashtags) do
175
+ assert_equal expected, valid_hashtag?(text), description
176
+ end
177
+
178
+ def_conformance_test("validate.yml", :lengths) do
179
+ assert_equal expected, tweet_length(text), description
181
180
  end
182
181
  end
data/twitter-text.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "twitter-text"
5
- s.version = "1.4.17"
5
+ s.version = "1.5.0"
6
6
  s.authors = ["Matt Sanford", "Patrick Ewing", "Ben Cherry", "Britt Selvitelle",
7
7
  "Raffi Krikorian", "J.P. Cummins", "Yoshimasa Niwa", "Keita Fujii"]
8
8
  s.email = ["matt@twitter.com", "patrick.henry.ewing@gmail.com", "bcherry@gmail.com", "bs@brittspace.com",
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twitter-text
3
3
  version: !ruby/object:Gem::Version
4
- hash: 37
4
+ hash: 3
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
- - 4
9
- - 17
10
- version: 1.4.17
8
+ - 5
9
+ - 0
10
+ version: 1.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matt Sanford
@@ -22,7 +22,7 @@ autorequire:
22
22
  bindir: bin
23
23
  cert_chain: []
24
24
 
25
- date: 2012-02-23 00:00:00 -08:00
25
+ date: 2012-06-18 00:00:00 -07:00
26
26
  default_executable:
27
27
  dependencies:
28
28
  - !ruby/object:Gem::Dependency
@@ -130,19 +130,21 @@ files:
130
130
  - .gitignore
131
131
  - .gitmodules
132
132
  - .rspec
133
+ - .travis.yml
133
134
  - Gemfile
134
135
  - LICENSE
135
136
  - README.rdoc
136
137
  - Rakefile
137
138
  - TODO
138
- - lib/autolink.rb
139
- - lib/extractor.rb
140
- - lib/hithighlighter.rb
141
- - lib/regex.rb
142
- - lib/rewriter.rb
143
139
  - lib/twitter-text.rb
144
- - lib/unicode.rb
145
- - lib/validation.rb
140
+ - lib/twitter-text/autolink.rb
141
+ - lib/twitter-text/deprecation.rb
142
+ - lib/twitter-text/extractor.rb
143
+ - lib/twitter-text/hit_highlighter.rb
144
+ - lib/twitter-text/regex.rb
145
+ - lib/twitter-text/rewriter.rb
146
+ - lib/twitter-text/unicode.rb
147
+ - lib/twitter-text/validation.rb
146
148
  - script/destroy
147
149
  - script/generate
148
150
  - spec/autolinking_spec.rb
data/lib/autolink.rb DELETED
@@ -1,266 +0,0 @@
1
- # encoding: UTF-8
2
-
3
- require 'set'
4
-
5
- module Twitter
6
- # A module for including Tweet auto-linking in a class. The primary use of this is for helpers/views so they can auto-link
7
- # usernames, lists, hashtags and URLs.
8
- module Autolink extend self
9
- # Default CSS class for auto-linked URLs
10
- DEFAULT_URL_CLASS = "tweet-url"
11
- # Default CSS class for auto-linked lists (along with the url class)
12
- DEFAULT_LIST_CLASS = "list-slug"
13
- # Default CSS class for auto-linked usernames (along with the url class)
14
- DEFAULT_USERNAME_CLASS = "username"
15
- # Default CSS class for auto-linked hashtags (along with the url class)
16
- DEFAULT_HASHTAG_CLASS = "hashtag"
17
- # Default target for auto-linked urls (nil will not add a target attribute)
18
- DEFAULT_TARGET = nil
19
- # HTML attribute for robot nofollow behavior (default)
20
- HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""
21
- # Options which should not be passed as HTML attributes
22
- OPTIONS_NOT_ATTRIBUTES = [:url_class, :list_class, :username_class, :hashtag_class,
23
- :username_url_base, :list_url_base, :hashtag_url_base,
24
- :username_url_block, :list_url_block, :hashtag_url_block, :link_url_block,
25
- :username_include_symbol, :suppress_lists, :suppress_no_follow, :url_entities]
26
-
27
- HTML_ENTITIES = {
28
- '&' => '&amp;',
29
- '>' => '&gt;',
30
- '<' => '&lt;',
31
- '"' => '&quot;',
32
- "'" => '&#39;'
33
- }
34
-
35
- def html_escape(text)
36
- text && text.to_s.gsub(/[&"'><]/) do |character|
37
- HTML_ENTITIES[character]
38
- end
39
- end
40
-
41
- # Add <tt><a></a></tt> tags around the usernames, lists, hashtags and URLs in the provided <tt>text</tt>. The
42
- # <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt>
43
- # hash:
44
- #
45
- # <tt>:url_class</tt>:: class to add to all <tt><a></tt> tags
46
- # <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
47
- # <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
48
- # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
49
- # <tt>:username_url_base</tt>:: the value for <tt>href</tt> attribute on username links. The <tt>@username</tt> (minus the <tt>@</tt>) will be appended at the end of this.
50
- # <tt>:list_url_base</tt>:: the value for <tt>href</tt> attribute on list links. The <tt>@username/list</tt> (minus the <tt>@</tt>) will be appended at the end of this.
51
- # <tt>:hashtag_url_base</tt>:: the value for <tt>href</tt> attribute on hashtag links. The <tt>#hashtag</tt> (minus the <tt>#</tt>) will be appended at the end of this.
52
- # <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
53
- # <tt>:suppress_lists</tt>:: disable auto-linking to lists
54
- # <tt>:suppress_no_follow</tt>:: Do not add <tt>rel="nofollow"</tt> to auto-linked items
55
- # <tt>:target</tt>:: add <tt>target="window_name"</tt> to auto-linked items
56
- def auto_link(text, options = {})
57
- auto_link_usernames_or_lists(
58
- auto_link_urls_custom(
59
- auto_link_hashtags(text, options),
60
- options),
61
- options)
62
- end
63
-
64
- # Add <tt><a></a></tt> tags around the usernames and lists in the provided <tt>text</tt>. The
65
- # <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt>
66
- # hash:
67
- #
68
- # <tt>:url_class</tt>:: class to add to all <tt><a></tt> tags
69
- # <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
70
- # <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
71
- # <tt>:username_url_base</tt>:: the value for <tt>href</tt> attribute on username links. The <tt>@username</tt> (minus the <tt>@</tt>) will be appended at the end of this.
72
- # <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
73
- # <tt>:list_url_base</tt>:: the value for <tt>href</tt> attribute on list links. The <tt>@username/list</tt> (minus the <tt>@</tt>) will be appended at the end of this.
74
- # <tt>:suppress_lists</tt>:: disable auto-linking to lists
75
- # <tt>:suppress_no_follow</tt>:: Do not add <tt>rel="nofollow"</tt> to auto-linked items
76
- # <tt>:target</tt>:: add <tt>target="window_name"</tt> to auto-linked items
77
- def auto_link_usernames_or_lists(text, options = {}) # :yields: list_or_username
78
- options = options.dup
79
- options[:url_class] ||= DEFAULT_URL_CLASS
80
- options[:list_class] ||= DEFAULT_LIST_CLASS
81
- options[:username_class] ||= DEFAULT_USERNAME_CLASS
82
- options[:username_url_base] ||= "https://twitter.com/"
83
- options[:list_url_base] ||= "https://twitter.com/"
84
- options[:target] ||= DEFAULT_TARGET
85
-
86
- extra_html = HTML_ATTR_NO_FOLLOW unless options[:suppress_no_follow]
87
-
88
- Twitter::Rewriter.rewrite_usernames_or_lists(text) do |at, username, slash_listname|
89
- at_before_user = options[:username_include_symbol] ? at : ''
90
- at = options[:username_include_symbol] ? '' : at
91
-
92
- name = "#{username}#{slash_listname}"
93
- chunk = block_given? ? yield(name) : name
94
-
95
- if slash_listname && !options[:suppress_lists]
96
- href = if options[:list_url_block]
97
- options[:list_url_block].call(name.downcase)
98
- else
99
- "#{html_escape(options[:list_url_base] + name.downcase)}"
100
- end
101
- %(#{at}<a class="#{options[:url_class]} #{options[:list_class]}" #{target_tag(options)}href="#{href}"#{extra_html}>#{html_escape(at_before_user + chunk)}</a>)
102
- else
103
- href = if options[:username_url_block]
104
- options[:username_url_block].call(chunk)
105
- else
106
- "#{html_escape(options[:username_url_base] + chunk)}"
107
- end
108
- %(#{at}<a class="#{options[:url_class]} #{options[:username_class]}" #{target_tag(options)}href="#{href}"#{extra_html}>#{html_escape(at_before_user + chunk)}</a>)
109
- end
110
- end
111
- end
112
-
113
- # Add <tt><a></a></tt> tags around the hashtags in the provided <tt>text</tt>. The
114
- # <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt>
115
- # hash:
116
- #
117
- # <tt>:url_class</tt>:: class to add to all <tt><a></tt> tags
118
- # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
119
- # <tt>:hashtag_url_base</tt>:: the value for <tt>href</tt> attribute. The hashtag text (minus the <tt>#</tt>) will be appended at the end of this.
120
- # <tt>:suppress_no_follow</tt>:: Do not add <tt>rel="nofollow"</tt> to auto-linked items
121
- # <tt>:target</tt>:: add <tt>target="window_name"</tt> to auto-linked items
122
- def auto_link_hashtags(text, options = {}) # :yields: hashtag_text
123
- options = options.dup
124
- options[:url_class] ||= DEFAULT_URL_CLASS
125
- options[:hashtag_class] ||= DEFAULT_HASHTAG_CLASS
126
- options[:hashtag_url_base] ||= "https://twitter.com/#!/search?q=%23"
127
- options[:target] ||= DEFAULT_TARGET
128
- extra_html = HTML_ATTR_NO_FOLLOW unless options[:suppress_no_follow]
129
-
130
- Twitter::Rewriter.rewrite_hashtags(text) do |hash, hashtag|
131
- hashtag = yield(hashtag) if block_given?
132
- href = if options[:hashtag_url_block]
133
- options[:hashtag_url_block].call(hashtag)
134
- else
135
- "#{options[:hashtag_url_base]}#{html_escape(hashtag)}"
136
- end
137
- %(<a href="#{href}" title="##{html_escape(hashtag)}" #{target_tag(options)}class="#{options[:url_class]} #{options[:hashtag_class]}"#{extra_html}>#{html_escape(hash)}#{html_escape(hashtag)}</a>)
138
- end
139
- end
140
-
141
- # Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>. Any
142
- # elements in the <tt>href_options</tt> hash will be converted to HTML attributes
143
- # and place in the <tt><a></tt> tag. Unless <tt>href_options</tt> contains <tt>:suppress_no_follow</tt>
144
- # the <tt>rel="nofollow"</tt> attribute will be added.
145
- def auto_link_urls_custom(text, href_options = {})
146
- options = href_options.dup
147
- options[:rel] = "nofollow" unless options.delete(:suppress_no_follow)
148
- options[:class] = options.delete(:url_class)
149
-
150
- url_entities = {}
151
- if options[:url_entities]
152
- options[:url_entities].each do |entity|
153
- url_entities[entity["url"]] = entity
154
- end
155
- options.delete(:url_entities)
156
- end
157
-
158
- Twitter::Rewriter.rewrite_urls(text) do |url|
159
- # In the case of t.co URLs, don't allow additional path characters
160
- after = ""
161
- if url =~ Twitter::Regex[:valid_tco_url]
162
- url = $&
163
- after = $'
164
- end
165
-
166
- href = if options[:link_url_block]
167
- options.delete(:link_url_block).call(url)
168
- else
169
- html_escape(url)
170
- end
171
-
172
- display_url = url
173
- link_text = html_escape(display_url)
174
- if url_entities[url] && url_entities[url]["display_url"]
175
- display_url = url_entities[url]["display_url"]
176
- expanded_url = url_entities[url]["expanded_url"]
177
- if !options[:title]
178
- options[:title] = expanded_url
179
- end
180
-
181
- # Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
182
- # should contain the full original URL (expanded_url), not the display URL.
183
- #
184
- # Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
185
- # font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
186
- # Elements with font-size:0 get copied even though they are not visible.
187
- # Note that display:none doesn't work here. Elements with display:none don't get copied.
188
- #
189
- # Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
190
- # wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
191
- # everything with the tco-ellipsis class.
192
- #
193
- # Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/#!/username/status/1234/photo/1
194
- # For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
195
- # For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
196
- display_url_sans_ellipses = display_url.sub("…", "")
197
- if expanded_url.include?(display_url_sans_ellipses)
198
- display_url_index = expanded_url.index(display_url_sans_ellipses)
199
- before_display_url = expanded_url.slice(0, display_url_index)
200
- # Portion of expanded_url that comes after display_url
201
- after_display_url = expanded_url.slice(display_url_index + display_url_sans_ellipses.length, 999999)
202
- preceding_ellipsis = display_url.match(/^…/) ? "…" : ""
203
- following_ellipsis = display_url.match(/…$/) ? "…" : ""
204
- # As an example: The user tweets "hi http://longdomainname.com/foo"
205
- # This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
206
- # This will get rendered as:
207
- # <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
208
- # …
209
- # <!-- There's a chance the onCopy event handler might not fire. In case that happens,
210
- # we include an &nbsp; here so that the … doesn't bump up against the URL and ruin it.
211
- # The &nbsp; is inside the tco-ellipsis span so that when the onCopy handler *does*
212
- # fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
213
- # e.g. "hi http://longdomainname.com/foo".
214
- # <span style='font-size:0'>&nbsp;</span>
215
- # </span>
216
- # <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
217
- # http://longdomai
218
- # </span>
219
- # <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
220
- # nname.com/foo
221
- # </span>
222
- # <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
223
- # <span style='font-size:0'>&nbsp;</span>
224
- # …
225
- # </span>
226
- invisible = "style='font-size:0; line-height:0'"
227
- link_text = "<span class='tco-ellipsis'>#{preceding_ellipsis}<span #{invisible}>&nbsp;</span></span><span #{invisible}>#{html_escape before_display_url}</span><span class='js-display-url'>#{html_escape display_url_sans_ellipses}</span><span #{invisible}>#{after_display_url}</span><span class='tco-ellipsis'><span #{invisible}>&nbsp;</span>#{following_ellipsis}</span>"
228
- end
229
- end
230
-
231
- html_attrs = html_attrs_for_options(options)
232
-
233
- %(<a href="#{href}"#{html_attrs}>#{link_text}</a>#{after})
234
- end
235
- end
236
-
237
- private
238
-
239
- BOOLEAN_ATTRIBUTES = Set.new([:disabled, :readonly, :multiple, :checked]).freeze
240
-
241
- def html_attrs_for_options(options)
242
- autolink_html_attrs options.reject{|k, v| OPTIONS_NOT_ATTRIBUTES.include?(k)}
243
- end
244
-
245
- def autolink_html_attrs(options)
246
- options.inject("") do |attrs, (key, value)|
247
- if BOOLEAN_ATTRIBUTES.include?(key)
248
- value = value ? key : nil
249
- end
250
- if !value.nil?
251
- attrs << %( #{html_escape(key)}="#{html_escape(value)}")
252
- end
253
- attrs
254
- end
255
- end
256
-
257
- def target_tag(options)
258
- target_option = options[:target].to_s
259
- if target_option.empty?
260
- ""
261
- else
262
- "target=\"#{html_escape(target_option)}\""
263
- end
264
- end
265
- end
266
- end