twitter-text 1.4.17 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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