twitter-text-simpleidn 3.0.0.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.
@@ -0,0 +1,69 @@
1
+ # Copyright 2018 Twitter, Inc.
2
+ # Licensed under the Apache License, Version 2.0
3
+ # http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ module Twitter
6
+ module TwitterText
7
+ # A module provides base methods to rewrite usernames, lists, hashtags and URLs.
8
+ module Rewriter extend self
9
+ def rewrite_entities(text, entities)
10
+ codepoints = text.to_s.to_codepoint_a
11
+
12
+ # sort by start index
13
+ entities = entities.sort_by do |entity|
14
+ indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
15
+ indices.first
16
+ end
17
+
18
+ result = []
19
+ last_index = entities.inject(0) do |index, entity|
20
+ indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
21
+ result << codepoints[index...indices.first]
22
+ result << yield(entity, codepoints)
23
+ indices.last
24
+ end
25
+ result << codepoints[last_index..-1]
26
+
27
+ result.flatten.join
28
+ end
29
+
30
+ # These methods are deprecated, will be removed in future.
31
+ extend Deprecation
32
+
33
+ def rewrite(text, options = {})
34
+ [:hashtags, :urls, :usernames_or_lists].inject(text) do |key|
35
+ options[key] ? send(:"rewrite_#{key}", text, &options[key]) : text
36
+ end
37
+ end
38
+ deprecate :rewrite, :rewrite_entities
39
+
40
+ def rewrite_usernames_or_lists(text)
41
+ entities = Extractor.extract_mentions_or_lists_with_indices(text)
42
+ rewrite_entities(text, entities) do |entity, codepoints|
43
+ at = codepoints[entity[:indices].first]
44
+ list_slug = entity[:list_slug]
45
+ list_slug = nil if list_slug.empty?
46
+ yield(at, entity[:screen_name], list_slug)
47
+ end
48
+ end
49
+ deprecate :rewrite_usernames_or_lists, :rewrite_entities
50
+
51
+ def rewrite_hashtags(text)
52
+ entities = Extractor.extract_hashtags_with_indices(text)
53
+ rewrite_entities(text, entities) do |entity, codepoints|
54
+ hash = codepoints[entity[:indices].first]
55
+ yield(hash, entity[:hashtag])
56
+ end
57
+ end
58
+ deprecate :rewrite_hashtags, :rewrite_entities
59
+
60
+ def rewrite_urls(text)
61
+ entities = Extractor.extract_urls_with_indices(text, :extract_url_without_protocol => false)
62
+ rewrite_entities(text, entities) do |entity, codepoints|
63
+ yield(entity[:url])
64
+ end
65
+ end
66
+ deprecate :rewrite_urls, :rewrite_entities
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2018 Twitter, Inc.
2
+ # Licensed under the Apache License, Version 2.0
3
+ # http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ module Twitter
6
+ module TwitterText
7
+ # This module lazily defines constants of the form Uxxxx for all Unicode
8
+ # codepoints from U0000 to U10FFFF. The value of each constant is the
9
+ # UTF-8 string for the codepoint.
10
+ # Examples:
11
+ # copyright = Unicode::U00A9
12
+ # euro = Unicode::U20AC
13
+ # infinity = Unicode::U221E
14
+ #
15
+ module Unicode
16
+ CODEPOINT_REGEX = /^U_?([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/
17
+
18
+ def self.const_missing(name)
19
+ # Check that the constant name is of the right form: U0000 to U10FFFF
20
+ if name.to_s =~ CODEPOINT_REGEX
21
+ # Convert the codepoint to an immutable UTF-8 string,
22
+ # define a real constant for that value and return the value
23
+ #p name, name.class
24
+ const_set(name, [$1.to_i(16)].pack("U").freeze)
25
+ else # Raise an error for constants that are not Unicode.
26
+ raise NameError, "Uninitialized constant: Unicode::#{name}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,251 @@
1
+ # Copyright 2018 Twitter, Inc.
2
+ # Licensed under the Apache License, Version 2.0
3
+ # http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ require 'unf'
6
+
7
+ module Twitter
8
+ module TwitterText
9
+ module Validation extend self
10
+ DEFAULT_TCO_URL_LENGTHS = {
11
+ :short_url_length => 23,
12
+ }
13
+
14
+ # :weighted_length the weighted length of tweet based on weights specified in the config
15
+ # :valid If tweet is valid
16
+ # :permillage permillage of the tweet over the max length specified in config
17
+ # :valid_range_start beginning of valid text
18
+ # :valid_range_end End index of valid part of the tweet text (inclusive)
19
+ # :display_range_start beginning index of display text
20
+ # :display_range_end end index of display text (inclusive)
21
+ class ParseResults < Hash
22
+
23
+ RESULT_PARAMS = [:weighted_length, :valid, :permillage, :valid_range_start, :valid_range_end, :display_range_start, :display_range_end]
24
+
25
+ def self.empty
26
+ return ParseResults.new(weighted_length: 0, permillage: 0, valid: true, display_range_start: 0, display_range_end: 0, valid_range_start: 0, valid_range_end: 0)
27
+ end
28
+
29
+ def initialize(params = {})
30
+ RESULT_PARAMS.each do |key|
31
+ super[key] = params[key] if params.key?(key)
32
+ end
33
+ end
34
+ end
35
+
36
+ # Parse input text and return hash with descriptive parameters populated.
37
+ def parse_tweet(text, options = {})
38
+ options = DEFAULT_TCO_URL_LENGTHS.merge(options)
39
+ config = options[:config] || Twitter::TwitterText::Configuration.default_configuration
40
+ normalized_text = text.to_nfc
41
+ unless (normalized_text.length > 0)
42
+ ParseResults.empty()
43
+ end
44
+
45
+ scale = config.scale
46
+ max_weighted_tweet_length = config.max_weighted_tweet_length
47
+ scaled_max_weighted_tweet_length = max_weighted_tweet_length * scale
48
+ transformed_url_length = config.transformed_url_length * scale
49
+ ranges = config.ranges
50
+
51
+ url_entities = Twitter::TwitterText::Extractor.extract_urls_with_indices(normalized_text)
52
+ emoji_entities = config.emoji_parsing_enabled ? Twitter::TwitterText::Extractor.extract_emoji_with_indices(normalized_text) : []
53
+
54
+ has_invalid_chars = false
55
+ weighted_count = 0
56
+ offset = 0
57
+ display_offset = 0
58
+ valid_offset = 0
59
+
60
+ while offset < normalized_text.codepoint_length
61
+ # Reset the default char weight each pass through the loop
62
+ char_weight = config.default_weight
63
+ entity_length = 0
64
+
65
+ url_entities.each do |url_entity|
66
+ if url_entity[:indices].first == offset
67
+ entity_length = url_entity[:indices].last - url_entity[:indices].first
68
+ weighted_count += transformed_url_length
69
+ offset += entity_length
70
+ display_offset += entity_length
71
+ if weighted_count <= scaled_max_weighted_tweet_length
72
+ valid_offset += entity_length
73
+ end
74
+ # Finding a match breaks the loop
75
+ break
76
+ end
77
+ end
78
+
79
+ emoji_entities.each do |emoji_entity|
80
+ if emoji_entity[:indices].first == offset
81
+ entity_length = emoji_entity[:indices].last - emoji_entity[:indices].first
82
+ weighted_count += char_weight # the default weight
83
+ offset += entity_length
84
+ display_offset += entity_length
85
+ if weighted_count <= scaled_max_weighted_tweet_length
86
+ valid_offset += entity_length
87
+ end
88
+ # Finding a match breaks the loop
89
+ break
90
+ end
91
+ end
92
+
93
+ next if entity_length > 0
94
+
95
+ if offset < normalized_text.codepoint_length
96
+ code_point = normalized_text[offset]
97
+
98
+ ranges.each do |range|
99
+ if range.contains?(code_point.unpack("U").first)
100
+ char_weight = range.weight
101
+ break
102
+ end
103
+ end
104
+
105
+ weighted_count += char_weight
106
+
107
+ has_invalid_chars = contains_invalid?(code_point) unless has_invalid_chars
108
+ codepoint_length = code_point.codepoint_length
109
+ offset += codepoint_length
110
+ display_offset += codepoint_length
111
+ # index += codepoint_length
112
+
113
+ if !has_invalid_chars && (weighted_count <= scaled_max_weighted_tweet_length)
114
+ valid_offset += codepoint_length
115
+ end
116
+ end
117
+ end
118
+
119
+ normalized_text_offset = text.codepoint_length - normalized_text.codepoint_length
120
+ scaled_weighted_length = weighted_count / scale
121
+ is_valid = !has_invalid_chars && (scaled_weighted_length <= max_weighted_tweet_length)
122
+ permillage = scaled_weighted_length * 1000 / max_weighted_tweet_length
123
+
124
+ return ParseResults.new(weighted_length: scaled_weighted_length, permillage: permillage, valid: is_valid, display_range_start: 0, display_range_end: (display_offset + normalized_text_offset - 1), valid_range_start: 0, valid_range_end: (valid_offset + normalized_text_offset - 1))
125
+ end
126
+
127
+ def contains_invalid?(text)
128
+ return false if !text || text.empty?
129
+ begin
130
+ return true if Twitter::TwitterText::Regex::INVALID_CHARACTERS.any?{|invalid_char| text.include?(invalid_char) }
131
+ rescue ArgumentError
132
+ # non-Unicode value.
133
+ return true
134
+ end
135
+ return false
136
+ end
137
+
138
+ def valid_username?(username)
139
+ return false if !username || username.empty?
140
+
141
+ extracted = Twitter::TwitterText::Extractor.extract_mentioned_screen_names(username)
142
+ # Should extract the username minus the @ sign, hence the [1..-1]
143
+ extracted.size == 1 && extracted.first == username[1..-1]
144
+ end
145
+
146
+ VALID_LIST_RE = /\A#{Twitter::TwitterText::Regex[:valid_mention_or_list]}\z/o
147
+ def valid_list?(username_list)
148
+ match = username_list.match(VALID_LIST_RE)
149
+ # Must have matched and had nothing before or after
150
+ !!(match && match[1] == "" && match[4] && !match[4].empty?)
151
+ end
152
+
153
+ def valid_hashtag?(hashtag)
154
+ return false if !hashtag || hashtag.empty?
155
+
156
+ extracted = Twitter::TwitterText::Extractor.extract_hashtags(hashtag)
157
+ # Should extract the hashtag minus the # sign, hence the [1..-1]
158
+ extracted.size == 1 && extracted.first == hashtag[1..-1]
159
+ end
160
+
161
+ def valid_url?(url, unicode_domains=true, require_protocol=true)
162
+ return false if !url || url.empty?
163
+
164
+ url_parts = url.match(Twitter::TwitterText::Regex[:validate_url_unencoded])
165
+ return false unless (url_parts && url_parts.to_s == url)
166
+
167
+ scheme, authority, path, query, fragment = url_parts.captures
168
+
169
+ return false unless ((!require_protocol ||
170
+ (valid_match?(scheme, Twitter::TwitterText::Regex[:validate_url_scheme]) && scheme.match(/\Ahttps?\Z/i))) &&
171
+ valid_match?(path, Twitter::TwitterText::Regex[:validate_url_path]) &&
172
+ valid_match?(query, Twitter::TwitterText::Regex[:validate_url_query], true) &&
173
+ valid_match?(fragment, Twitter::TwitterText::Regex[:validate_url_fragment], true))
174
+
175
+ return (unicode_domains && valid_match?(authority, Twitter::TwitterText::Regex[:validate_url_unicode_authority])) ||
176
+ (!unicode_domains && valid_match?(authority, Twitter::TwitterText::Regex[:validate_url_authority]))
177
+ end
178
+
179
+ # These methods are deprecated, will be removed in future.
180
+ extend Deprecation
181
+
182
+ MAX_LENGTH_LEGACY = 140
183
+
184
+ # DEPRECATED: Please use parse_text instead.
185
+ #
186
+ # Returns the length of the string as it would be displayed. This is equivilent to the length of the Unicode NFC
187
+ # (See: http://www.unicode.org/reports/tr15). This is needed in order to consistently calculate the length of a
188
+ # string no matter which actual form was transmitted. For example:
189
+ #
190
+ # U+0065 Latin Small Letter E
191
+ # + U+0301 Combining Acute Accent
192
+ # ----------
193
+ # = 2 bytes, 2 characters, displayed as é (1 visual glyph)
194
+ # … The NFC of {U+0065, U+0301} is {U+00E9}, which is a single chracter and a +display_length+ of 1
195
+ #
196
+ # The string could also contain U+00E9 already, in which case the canonicalization will not change the value.
197
+ #
198
+ def tweet_length(text, options = {})
199
+ options = DEFAULT_TCO_URL_LENGTHS.merge(options)
200
+
201
+ length = text.to_nfc.unpack("U*").length
202
+
203
+ Twitter::TwitterText::Extractor.extract_urls_with_indices(text) do |url, start_position, end_position|
204
+ length += start_position - end_position
205
+ length += options[:short_url_length] if url.length > 0
206
+ end
207
+
208
+ length
209
+ end
210
+ deprecate :tweet_length, :parse_tweet
211
+
212
+ # DEPRECATED: Please use parse_text instead.
213
+ #
214
+ # Check the <tt>text</tt> for any reason that it may not be valid as a Tweet. This is meant as a pre-validation
215
+ # before posting to api.twitter.com. There are several server-side reasons for Tweets to fail but this pre-validation
216
+ # will allow quicker feedback.
217
+ #
218
+ # Returns <tt>false</tt> if this <tt>text</tt> is valid. Otherwise one of the following Symbols will be returned:
219
+ #
220
+ # <tt>:too_long</tt>:: if the <tt>text</tt> is too long
221
+ # <tt>:empty</tt>:: if the <tt>text</tt> is nil or empty
222
+ # <tt>:invalid_characters</tt>:: if the <tt>text</tt> contains non-Unicode or any of the disallowed Unicode characters
223
+ def tweet_invalid?(text)
224
+ return :empty if !text || text.empty?
225
+ begin
226
+ return :too_long if tweet_length(text) > MAX_LENGTH_LEGACY
227
+ return :invalid_characters if Twitter::TwitterText::Regex::INVALID_CHARACTERS.any?{|invalid_char| text.include?(invalid_char) }
228
+ rescue ArgumentError
229
+ # non-Unicode value.
230
+ return :invalid_characters
231
+ end
232
+
233
+ return false
234
+ end
235
+ deprecate :tweet_invalid?, :parse_tweet
236
+
237
+ def valid_tweet_text?(text)
238
+ !tweet_invalid?(text)
239
+ end
240
+ deprecate :valid_tweet_text?, :parse_tweet
241
+
242
+ private
243
+
244
+ def valid_match?(string, regex, optional=false)
245
+ return (string && string.match(regex) && $~.to_s == string) unless optional
246
+
247
+ !(string && (!string.match(regex) || $~.to_s != string))
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright 2018 Twitter, Inc.
2
+ # Licensed under the Apache License, Version 2.0
3
+ # http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ # encoding: UTF-8
6
+
7
+ module Twitter
8
+ module TwitterText
9
+ class WeightedRange
10
+ attr_reader :start, :end, :weight
11
+
12
+ def initialize(range = {})
13
+ raise ArgumentError.new("Invalid range") unless [:start, :end, :weight].all? { |key| range.key?(key) && range[key].is_a?(Integer) }
14
+ @start = range[:start]
15
+ @end = range[:end]
16
+ @weight = range[:weight]
17
+ end
18
+
19
+ def contains?(code_point)
20
+ code_point >= @start && code_point <= @end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:newgem_simple, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:newgem_simple, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,848 @@
1
+ # Copyright 2018 Twitter, Inc.
2
+ # Licensed under the Apache License, Version 2.0
3
+ # http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ # encoding: utf-8
6
+ require File.dirname(__FILE__) + '/spec_helper'
7
+
8
+ class TestAutolink
9
+ include Twitter::TwitterText::Autolink
10
+ end
11
+
12
+ describe Twitter::TwitterText::Autolink do
13
+ def original_text; end
14
+ def url; end
15
+
16
+ describe "auto_link_custom" do
17
+ before do
18
+ @autolinked_text = TestAutolink.new.auto_link(original_text) if original_text
19
+ end
20
+
21
+ describe "username autolinking" do
22
+ context "username preceded by a space" do
23
+ def original_text; "hello @jacob"; end
24
+
25
+ it "should be linked" do
26
+ expect(@autolinked_text).to link_to_screen_name('jacob')
27
+ end
28
+ end
29
+
30
+ context "username in camelCase" do
31
+ def original_text() "@jaCob iS cOoL" end
32
+
33
+ it "should be linked" do
34
+ expect(@autolinked_text).to link_to_screen_name('jaCob')
35
+ end
36
+ end
37
+
38
+ context "username at beginning of line" do
39
+ def original_text; "@jacob you're cool"; end
40
+
41
+ it "should be linked" do
42
+ expect(@autolinked_text).to link_to_screen_name('jacob')
43
+ end
44
+ end
45
+
46
+ context "username preceded by word character" do
47
+ def original_text; "meet@the beach"; end
48
+
49
+ it "should not be linked" do
50
+ expect(Nokogiri::HTML(@autolinked_text).search('a')).to be_empty
51
+ end
52
+ end
53
+
54
+ context "username preceded by non-word character" do
55
+ def original_text; "great.@jacob"; end
56
+
57
+ it "should be linked" do
58
+ expect(@autolinked_text).to link_to_screen_name('jacob')
59
+ end
60
+ end
61
+
62
+ context "username containing non-word characters" do
63
+ def original_text; "@zach&^$%^"; end
64
+
65
+ it "should not be linked" do
66
+ expect(@autolinked_text).to link_to_screen_name('zach')
67
+ end
68
+ end
69
+
70
+ context "username over twenty characters" do
71
+ def original_text
72
+ @twenty_character_username = "zach" * 5
73
+ "@" + @twenty_character_username + "1"
74
+ end
75
+
76
+ it "should not be linked" do
77
+ expect(@autolinked_text).to link_to_screen_name(@twenty_character_username)
78
+ end
79
+ end
80
+
81
+ context "username followed by japanese" do
82
+ def original_text; "@jacobの"; end
83
+
84
+ it "should be linked" do
85
+ expect(@autolinked_text).to link_to_screen_name('jacob')
86
+ end
87
+ end
88
+
89
+ context "username preceded by japanese" do
90
+ def original_text; "あ@matz"; end
91
+
92
+ it "should be linked" do
93
+ expect(@autolinked_text).to link_to_screen_name('matz')
94
+ end
95
+ end
96
+
97
+ context "username surrounded by japanese" do
98
+ def original_text; "あ@yoshimiの"; end
99
+
100
+ it "should be linked" do
101
+ expect(@autolinked_text).to link_to_screen_name('yoshimi')
102
+ end
103
+ end
104
+
105
+ context "username using full-width at-sign" do
106
+ def original_text
107
+ "#{[0xFF20].pack('U')}jacob"
108
+ end
109
+
110
+ it "should be linked" do
111
+ expect(@autolinked_text).to link_to_screen_name('jacob')
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "list path autolinking" do
117
+
118
+ context "when List is not available" do
119
+ it "should not be linked" do
120
+ @autolinked_text = TestAutolink.new.auto_link_usernames_or_lists("hello @jacob/my-list", :suppress_lists => true)
121
+ expect(@autolinked_text).to_not link_to_list_path('jacob/my-list')
122
+ expect(@autolinked_text).to include('my-list')
123
+ end
124
+ end
125
+
126
+ context "slug preceded by a space" do
127
+ def original_text; "hello @jacob/my-list"; end
128
+
129
+ it "should be linked" do
130
+ expect(@autolinked_text).to link_to_list_path('jacob/my-list')
131
+ end
132
+ end
133
+
134
+ context "username followed by a slash but no list" do
135
+ def original_text; "hello @jacob/ my-list"; end
136
+
137
+ it "should NOT be linked" do
138
+ expect(@autolinked_text).to_not link_to_list_path('jacob/my-list')
139
+ expect(@autolinked_text).to link_to_screen_name('jacob')
140
+ end
141
+ end
142
+
143
+ context "empty username followed by a list" do
144
+ def original_text; "hello @/my-list"; end
145
+
146
+ it "should NOT be linked" do
147
+ expect(Nokogiri::HTML(@autolinked_text).search('a')).to be_empty
148
+ end
149
+ end
150
+
151
+ context "list slug at beginning of line" do
152
+ def original_text; "@jacob/my-list"; end
153
+
154
+ it "should be linked" do
155
+ expect(@autolinked_text).to link_to_list_path('jacob/my-list')
156
+ end
157
+ end
158
+
159
+ context "username preceded by alpha-numeric character" do
160
+ def original_text; "meet@the/beach"; end
161
+
162
+ it "should not be linked" do
163
+ expect(Nokogiri::HTML(@autolinked_text).search('a')).to be_empty
164
+ end
165
+ end
166
+
167
+ context "username preceded by non-word character" do
168
+ def original_text; "great.@jacob/my-list"; end
169
+
170
+ it "should be linked" do
171
+ @autolinked_text = TestAutolink.new.auto_link("great.@jacob/my-list")
172
+ expect(@autolinked_text).to link_to_list_path('jacob/my-list')
173
+ end
174
+ end
175
+
176
+ context "username containing non-word characters" do
177
+ def original_text; "@zach/test&^$%^"; end
178
+
179
+ it "should be linked" do
180
+ expect(@autolinked_text).to link_to_list_path('zach/test')
181
+ end
182
+ end
183
+
184
+ context "username over twenty characters" do
185
+ def original_text
186
+ @twentyfive_character_list = "jack/" + ("a" * 25)
187
+ "@#{@twentyfive_character_list}12345"
188
+ end
189
+
190
+ it "should be linked" do
191
+ expect(@autolinked_text).to link_to_list_path(@twentyfive_character_list)
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "hashtag autolinking" do
197
+ context "with an all numeric hashtag" do
198
+ def original_text; "#123"; end
199
+
200
+ it "should not be linked" do
201
+ expect(@autolinked_text).to_not have_autolinked_hashtag('#123')
202
+ end
203
+ end
204
+
205
+ context "with a hashtag with alphanumeric characters" do
206
+ def original_text; "#ab1d"; end
207
+
208
+ it "should be linked" do
209
+ expect(@autolinked_text).to have_autolinked_hashtag('#ab1d')
210
+ end
211
+ end
212
+
213
+ context "with a hashtag with underscores" do
214
+ def original_text; "#a_b_c_d"; end
215
+
216
+ it "should be linked" do
217
+ expect(@autolinked_text).to have_autolinked_hashtag(original_text)
218
+ end
219
+ end
220
+
221
+ context "with a hashtag that is preceded by a word character" do
222
+ def original_text; "ab#cd"; end
223
+
224
+ it "should not be linked" do
225
+ expect(@autolinked_text).to_not have_autolinked_hashtag(original_text)
226
+ end
227
+ end
228
+
229
+ context "with a page anchor in a url" do
230
+ def original_text; "Here's my url: http://foobar.com/#home"; end
231
+
232
+ it "should not link the hashtag" do
233
+ expect(@autolinked_text).to_not have_autolinked_hashtag('#home')
234
+ end
235
+
236
+ it "should link the url" do
237
+ expect(@autolinked_text).to have_autolinked_url('http://foobar.com/#home')
238
+ end
239
+ end
240
+
241
+ context "with a hashtag that starts with a number but has word characters" do
242
+ def original_text; "#2ab"; end
243
+
244
+ it "should be linked" do
245
+ expect(@autolinked_text).to have_autolinked_hashtag(original_text)
246
+ end
247
+ end
248
+
249
+ context "with multiple valid hashtags" do
250
+ def original_text; "I'm frickin' awesome #ab #cd #ef"; end
251
+
252
+ it "links each hashtag" do
253
+ expect(@autolinked_text).to have_autolinked_hashtag('#ab')
254
+ expect(@autolinked_text).to have_autolinked_hashtag('#cd')
255
+ expect(@autolinked_text).to have_autolinked_hashtag('#ef')
256
+ end
257
+ end
258
+
259
+ context "with a hashtag preceded by a ." do
260
+ def original_text; "ok, great.#abc"; end
261
+
262
+ it "should be linked" do
263
+ expect(@autolinked_text).to have_autolinked_hashtag('#abc')
264
+ end
265
+ end
266
+
267
+ context "with a hashtag preceded by a &" do
268
+ def original_text; "&#nbsp;"; end
269
+
270
+ it "should not be linked" do
271
+ expect(@autolinked_text).to_not have_autolinked_hashtag('#nbsp;')
272
+ end
273
+ end
274
+
275
+ context "with a hashtag that ends in an !" do
276
+ def original_text; "#great!"; end
277
+
278
+ it "should be linked, but should not include the !" do
279
+ expect(@autolinked_text).to have_autolinked_hashtag('#great')
280
+ end
281
+ end
282
+
283
+ context "with a hashtag followed by Japanese" do
284
+ def original_text; "#twj_devの"; end
285
+
286
+ it "should be linked" do
287
+ expect(@autolinked_text).to have_autolinked_hashtag('#twj_devの')
288
+ end
289
+ end
290
+
291
+ context "with a hashtag preceded by a full-width space" do
292
+ def original_text; "#{[0x3000].pack('U')}#twj_dev"; end
293
+
294
+ it "should be linked" do
295
+ expect(@autolinked_text).to have_autolinked_hashtag('#twj_dev')
296
+ end
297
+ end
298
+
299
+ context "with a hashtag followed by a full-width space" do
300
+ def original_text; "#twj_dev#{[0x3000].pack('U')}"; end
301
+
302
+ it "should be linked" do
303
+ expect(@autolinked_text).to have_autolinked_hashtag('#twj_dev')
304
+ end
305
+ end
306
+
307
+ context "with a hashtag using full-width hash" do
308
+ def original_text; "#{[0xFF03].pack('U')}twj_dev"; end
309
+
310
+ it "should be linked" do
311
+ link = Nokogiri::HTML(@autolinked_text).search('a')
312
+ expect((link.inner_text.respond_to?(:force_encoding) ? link.inner_text.force_encoding("utf-8") : link.inner_text)).to be == "#{[0xFF03].pack('U')}twj_dev"
313
+ expect(link.first['href']).to be == 'https://twitter.com/search?q=%23twj_dev'
314
+ end
315
+ end
316
+
317
+ context "with a hashtag containing an accented latin character" do
318
+ def original_text
319
+ # the hashtag is #éhashtag
320
+ "##{[0x00e9].pack('U')}hashtag"
321
+ end
322
+
323
+ it "should be linked" do
324
+ expect(@autolinked_text).to be == "<a class=\"tweet-url hashtag\" href=\"https://twitter.com/search?q=%23éhashtag\" rel=\"nofollow\" title=\"#éhashtag\">#éhashtag</a>"
325
+ end
326
+ end
327
+
328
+ end
329
+
330
+ describe "URL autolinking" do
331
+ def url; "http://www.google.com"; end
332
+
333
+ context "when embedded in plain text" do
334
+ def original_text; "On my search engine #{url} I found good links."; end
335
+
336
+ it "should be linked" do
337
+ expect(@autolinked_text).to have_autolinked_url(url)
338
+ end
339
+ end
340
+
341
+ context "when surrounded by Japanese;" do
342
+ def original_text; "いまなにしてる#{url}いまなにしてる"; end
343
+
344
+ it "should be linked" do
345
+ expect(@autolinked_text).to have_autolinked_url(url)
346
+ end
347
+ end
348
+
349
+ context "with a path surrounded by parentheses;" do
350
+ def original_text; "I found a neatness (#{url})"; end
351
+
352
+ it "should be linked" do
353
+ expect(@autolinked_text).to have_autolinked_url(url)
354
+ end
355
+
356
+ context "when the URL ends with a slash;" do
357
+ def url; "http://www.google.com/"; end
358
+
359
+ it "should be linked" do
360
+ expect(@autolinked_text).to have_autolinked_url(url)
361
+ end
362
+ end
363
+
364
+ context "when the URL has a path;" do
365
+ def url; "http://www.google.com/fsdfasdf"; end
366
+
367
+ it "should be linked" do
368
+ expect(@autolinked_text).to have_autolinked_url(url)
369
+ end
370
+ end
371
+ end
372
+
373
+ context "when path contains parens" do
374
+ def original_text; "I found a neatness (#{url})"; end
375
+
376
+ it "should be linked" do
377
+ expect(@autolinked_text).to have_autolinked_url(url)
378
+ end
379
+
380
+ context "wikipedia" do
381
+ def url; "http://en.wikipedia.org/wiki/Madonna_(artist)"; end
382
+
383
+ it "should be linked" do
384
+ expect(@autolinked_text).to have_autolinked_url(url)
385
+ end
386
+ end
387
+
388
+ context "IIS session" do
389
+ def url; "http://msdn.com/S(deadbeef)/page.htm"; end
390
+
391
+ it "should be linked" do
392
+ expect(@autolinked_text).to have_autolinked_url(url)
393
+ end
394
+ end
395
+
396
+ context "unbalanced parens" do
397
+ def url; "http://example.com/i_has_a_("; end
398
+
399
+ it "should be linked" do
400
+ expect(@autolinked_text).to have_autolinked_url("http://example.com/i_has_a_")
401
+ end
402
+ end
403
+
404
+ context "balanced parens with a double quote inside" do
405
+ def url; "http://foo.com/foo_(\")_bar" end
406
+
407
+ it "should be linked" do
408
+ expect(@autolinked_text).to have_autolinked_url("http://foo.com/foo_")
409
+ end
410
+ end
411
+
412
+ context "balanced parens hiding XSS" do
413
+ def url; 'http://x.xx.com/("style="color:red"onmouseover="alert(1)' end
414
+
415
+ it "should be linked" do
416
+ expect(@autolinked_text).to have_autolinked_url("http://x.xx.com/")
417
+ end
418
+ end
419
+ end
420
+
421
+ context "when preceded by a :" do
422
+ def original_text; "Check this out @hoverbird:#{url}"; end
423
+
424
+ it "should be linked" do
425
+ expect(@autolinked_text).to have_autolinked_url(url)
426
+ end
427
+ end
428
+
429
+ context "with a URL ending in allowed punctuation" do
430
+ it "does not consume ending punctuation" do
431
+ matcher = TestAutolink.new
432
+ %w| ? ! , . : ; ] ) } = \ ' |.each do |char|
433
+ expect(matcher.auto_link("#{url}#{char}")).to have_autolinked_url(url)
434
+ end
435
+ end
436
+ end
437
+
438
+ context "with a URL preceded in forbidden characters" do
439
+ it "should be linked" do
440
+ matcher = TestAutolink.new
441
+ %w| \ ' / ! = |.each do |char|
442
+ expect(matcher.auto_link("#{char}#{url}")).to have_autolinked_url(url)
443
+ end
444
+ end
445
+ end
446
+
447
+ context "when embedded in a link tag" do
448
+ def original_text; "<link rel='true'>#{url}</link>"; end
449
+
450
+ it "should be linked" do
451
+ expect(@autolinked_text).to have_autolinked_url(url)
452
+ end
453
+ end
454
+
455
+ context "with multiple URLs" do
456
+ def original_text; "http://www.links.org link at start of page, link at end http://www.foo.org"; end
457
+
458
+ it "should autolink each one" do
459
+ expect(@autolinked_text).to have_autolinked_url('http://www.links.org')
460
+ expect(@autolinked_text).to have_autolinked_url('http://www.foo.org')
461
+ end
462
+ end
463
+
464
+ context "with multiple URLs in different formats" do
465
+ def original_text; "http://foo.com https://bar.com http://mail.foobar.org"; end
466
+
467
+ it "should autolink each one, in the proper order" do
468
+ expect(@autolinked_text).to have_autolinked_url('http://foo.com')
469
+ expect(@autolinked_text).to have_autolinked_url('https://bar.com')
470
+ expect(@autolinked_text).to have_autolinked_url('http://mail.foobar.org')
471
+ end
472
+ end
473
+
474
+ context "with a URL having a long TLD" do
475
+ def original_text; "Yahoo integriert Facebook http://golem.mobi/0912/71607.html"; end
476
+
477
+ it "should autolink it" do
478
+ expect(@autolinked_text).to have_autolinked_url('http://golem.mobi/0912/71607.html')
479
+ end
480
+ end
481
+
482
+ context "with a url lacking the protocol" do
483
+ def original_text; "I like www.foobar.com dudes"; end
484
+
485
+ it "does not link at all" do
486
+ link = Nokogiri::HTML(@autolinked_text).search('a')
487
+ expect(link).to be_empty
488
+ end
489
+ end
490
+
491
+ context "with a @ in a URL" do
492
+ context "with XSS attack" do
493
+ def original_text; 'http://x.xx.com/@"style="color:pink"onmouseover=alert(1)//'; end
494
+
495
+ it "should not allow XSS follwing @" do
496
+ expect(@autolinked_text).to have_autolinked_url('http://x.xx.com/')
497
+ end
498
+ end
499
+
500
+ context "with a username not followed by a /" do
501
+ def original_text; 'http://example.com/@foobar'; end
502
+
503
+ it "should link url" do
504
+ expect(@autolinked_text).to have_autolinked_url('http://example.com/@foobar')
505
+ end
506
+ end
507
+
508
+ context "with a username followed by a /" do
509
+ def original_text; 'http://example.com/@foobar/'; end
510
+
511
+ it "should not link the username but link full url" do
512
+ expect(@autolinked_text).to have_autolinked_url('http://example.com/@foobar/')
513
+ expect(@autolinked_text).to_not link_to_screen_name('foobar')
514
+ end
515
+ end
516
+ end
517
+
518
+ context "regex engine quirks" do
519
+ context "does not spiral out of control on repeated periods" do
520
+ def original_text; "Test a ton of periods http://example.com/path.........................................."; end
521
+
522
+ it "should autolink" do
523
+ expect(@autolinked_text).to have_autolinked_url('http://example.com/path')
524
+ end
525
+ end
526
+
527
+ context "does not spiral out of control on repeated dashes" do
528
+ def original_text; "Single char file ext http://www.bestbuy.com/site/Currie+Technologies+-+Ezip+400+Scooter/9885188.p?id=1218189013070&skuId=9885188"; end
529
+
530
+ it "should autolink" do
531
+ expect(@autolinked_text).to have_autolinked_url('http://www.bestbuy.com/site/Currie+Technologies+-+Ezip+400+Scooter/9885188.p?id=1218189013070&skuId=9885188')
532
+ end
533
+ end
534
+ end
535
+
536
+ end
537
+
538
+ describe "Autolink all" do
539
+ before do
540
+ @linker = TestAutolink.new
541
+ end
542
+
543
+ it "should allow url/hashtag overlap" do
544
+ auto_linked = @linker.auto_link("https://twitter.com/#search")
545
+ expect(auto_linked).to have_autolinked_url('https://twitter.com/#search')
546
+ end
547
+
548
+ it "should not add invalid option in HTML tags" do
549
+ auto_linked = @linker.auto_link("https://twitter.com/ is a URL, not a hashtag", :hashtag_class => 'hashtag_classname')
550
+ expect(auto_linked).to have_autolinked_url('https://twitter.com/')
551
+ expect(auto_linked).to_not include('hashtag_class')
552
+ expect(auto_linked).to_not include('hashtag_classname')
553
+ end
554
+
555
+ it "should autolink url/hashtag/mention in text with Unicode supplementary characters" do
556
+ auto_linked = @linker.auto_link("#{[0x10400].pack('U')} #hashtag #{[0x10400].pack('U')} @mention #{[0x10400].pack('U')} http://twitter.com/")
557
+ expect(auto_linked).to have_autolinked_hashtag('#hashtag')
558
+ expect(auto_linked).to link_to_screen_name('mention')
559
+ expect(auto_linked).to have_autolinked_url('http://twitter.com/')
560
+ end
561
+ end
562
+
563
+ end
564
+
565
+ describe "autolinking options" do
566
+ before do
567
+ @linker = TestAutolink.new
568
+ end
569
+
570
+ it "should show display_url when :url_entities provided" do
571
+ linked = @linker.auto_link("http://t.co/0JG5Mcq", :url_entities => [{
572
+ "url" => "http://t.co/0JG5Mcq",
573
+ "display_url" => "blog.twitter.com/2011/05/twitte…",
574
+ "expanded_url" => "http://blog.twitter.com/2011/05/twitter-for-mac-update.html",
575
+ "indices" => [
576
+ 84,
577
+ 103
578
+ ]
579
+ }])
580
+ html = Nokogiri::HTML(linked)
581
+ expect(html.search('a')).to_not be_empty
582
+ expect(html.search('a[@href="http://t.co/0JG5Mcq"]')).to_not be_empty
583
+ expect(html.search('span[@class=js-display-url]').inner_text).to be == "blog.twitter.com/2011/05/twitte"
584
+ expect(html.inner_text).to be == " http://blog.twitter.com/2011/05/twitter-for-mac-update.html …"
585
+ expect(html.search('span[@style="position:absolute;left:-9999px;"]').size).to be == 4
586
+ end
587
+
588
+ it "should accept invisible_tag_attrs option" do
589
+ linked = @linker.auto_link("http://t.co/0JG5Mcq",
590
+ {
591
+ :url_entities => [{
592
+ "url" => "http://t.co/0JG5Mcq",
593
+ "display_url" => "blog.twitter.com/2011/05/twitte…",
594
+ "expanded_url" => "http://blog.twitter.com/2011/05/twitter-for-mac-update.html",
595
+ "indices" => [
596
+ 0,
597
+ 19
598
+ ]
599
+ }],
600
+ :invisible_tag_attrs => "style='dummy;'"
601
+ })
602
+ html = Nokogiri::HTML(linked)
603
+ expect(html.search('span[@style="dummy;"]').size).to be == 4
604
+ end
605
+
606
+ it "should show display_url if available in entity" do
607
+ linked = @linker.auto_link_entities("http://t.co/0JG5Mcq",
608
+ [{
609
+ :url => "http://t.co/0JG5Mcq",
610
+ :display_url => "blog.twitter.com/2011/05/twitte…",
611
+ :expanded_url => "http://blog.twitter.com/2011/05/twitter-for-mac-update.html",
612
+ :indices => [0, 19]
613
+ }]
614
+ )
615
+ html = Nokogiri::HTML(linked)
616
+ expect(html.search('a')).to_not be_empty
617
+ expect(html.search('a[@href="http://t.co/0JG5Mcq"]')).to_not be_empty
618
+ expect(html.search('span[@class=js-display-url]').inner_text).to be == "blog.twitter.com/2011/05/twitte"
619
+ expect(html.inner_text).to be == " http://blog.twitter.com/2011/05/twitter-for-mac-update.html …"
620
+ end
621
+
622
+ it "should apply :class as a CSS class" do
623
+ linked = @linker.auto_link("http://example.com/", :class => 'myclass')
624
+ expect(linked).to have_autolinked_url('http://example.com/')
625
+ expect(linked).to match(/myclass/)
626
+ end
627
+
628
+ it "should apply :url_class only on URL" do
629
+ linked = @linker.auto_link("http://twitter.com")
630
+ expect(linked).to have_autolinked_url('http://twitter.com')
631
+ expect(expect(linked)).to_not match(/class/)
632
+
633
+ linked = @linker.auto_link("http://twitter.com", :url_class => 'testClass')
634
+ expect(linked).to have_autolinked_url('http://twitter.com')
635
+ expect(linked).to match(/class=\"testClass\"/)
636
+
637
+ linked = @linker.auto_link("#hash @tw", :url_class => 'testClass')
638
+ expect(linked).to match(/class=\"tweet-url hashtag\"/)
639
+ expect(linked).to match(/class=\"tweet-url username\"/)
640
+ expect(linked).to_not match(/class=\"testClass\"/)
641
+ end
642
+
643
+ it "should add rel=nofollow by default" do
644
+ linked = @linker.auto_link("http://example.com/")
645
+ expect(linked).to have_autolinked_url('http://example.com/')
646
+ expect(linked).to match(/nofollow/)
647
+ end
648
+
649
+ it "should include the '@' symbol in a username when passed :username_include_symbol" do
650
+ linked = @linker.auto_link("@user", :username_include_symbol => true)
651
+ expect(linked).to link_to_screen_name('user', '@user')
652
+ end
653
+
654
+ it "should include the '@' symbol in a list when passed :username_include_symbol" do
655
+ linked = @linker.auto_link("@user/list", :username_include_symbol => true)
656
+ expect(linked).to link_to_list_path('user/list', '@user/list')
657
+ end
658
+
659
+ it "should not add rel=nofollow when passed :suppress_no_follow" do
660
+ linked = @linker.auto_link("http://example.com/", :suppress_no_follow => true)
661
+ expect(linked).to have_autolinked_url('http://example.com/')
662
+ expect(linked).to_not match(/nofollow/)
663
+ end
664
+
665
+ it "should not add a target attribute by default" do
666
+ linked = @linker.auto_link("http://example.com/")
667
+ expect(linked).to have_autolinked_url('http://example.com/')
668
+ expect(linked).to_not match(/target=/)
669
+ end
670
+
671
+ it "should respect the :target option" do
672
+ linked = @linker.auto_link("http://example.com/", :target => 'mywindow')
673
+ expect(linked).to have_autolinked_url('http://example.com/')
674
+ expect(linked).to match(/target="mywindow"/)
675
+ end
676
+
677
+ it "should customize href by username_url_block option" do
678
+ linked = @linker.auto_link("@test", :username_url_block => lambda{|a| "dummy"})
679
+ expect(linked).to have_autolinked_url('dummy', 'test')
680
+ end
681
+
682
+ it "should customize href by list_url_block option" do
683
+ linked = @linker.auto_link("@test/list", :list_url_block => lambda{|a| "dummy"})
684
+ expect(linked).to have_autolinked_url('dummy', 'test/list')
685
+ end
686
+
687
+ it "should customize href by hashtag_url_block option" do
688
+ linked = @linker.auto_link("#hashtag", :hashtag_url_block => lambda{|a| "dummy"})
689
+ expect(linked).to have_autolinked_url('dummy', '#hashtag')
690
+ end
691
+
692
+ it "should customize href by cashtag_url_block option" do
693
+ linked = @linker.auto_link("$CASH", :cashtag_url_block => lambda{|a| "dummy"})
694
+ expect(linked).to have_autolinked_url('dummy', '$CASH')
695
+ end
696
+
697
+ it "should customize href by link_url_block option" do
698
+ linked = @linker.auto_link("http://example.com/", :link_url_block => lambda{|a| "dummy"})
699
+ expect(linked).to have_autolinked_url('dummy', 'http://example.com/')
700
+ end
701
+
702
+ it "should modify link attributes by link_attribute_block" do
703
+ linked = @linker.auto_link("#hash @mention",
704
+ :link_attribute_block => lambda{|entity, attributes|
705
+ attributes[:"dummy-hash-attr"] = "test" if entity[:hashtag]
706
+ }
707
+ )
708
+ expect(linked).to match(/<a[^>]+hashtag[^>]+dummy-hash-attr=\"test\"[^>]+>/)
709
+ expect(linked).to_not match(/<a[^>]+username[^>]+dummy-hash-attr=\"test\"[^>]+>/)
710
+ expect(linked).to_not match(/link_attribute_block/i)
711
+
712
+ linked = @linker.auto_link("@mention http://twitter.com/",
713
+ :link_attribute_block => lambda{|entity, attributes|
714
+ attributes["dummy-url-attr"] = entity[:url] if entity[:url]
715
+ }
716
+ )
717
+ expect(linked).to_not match(/<a[^>]+username[^>]+dummy-url-attr=\"http:\/\/twitter.com\/\"[^>]*>/)
718
+ expect(linked).to match(/<a[^>]+dummy-url-attr=\"http:\/\/twitter.com\/\"/)
719
+ end
720
+
721
+ it "should modify link text by link_text_block" do
722
+ linked = @linker.auto_link("#hash @mention",
723
+ :link_text_block => lambda{|entity, text|
724
+ entity[:hashtag] ? "#replaced" : "pre_#{text}_post"
725
+ }
726
+ )
727
+ expect(linked).to match(/<a[^>]+>#replaced<\/a>/)
728
+ expect(linked).to match(/<a[^>]+>pre_mention_post<\/a>/)
729
+
730
+ linked = @linker.auto_link("#hash @mention", {
731
+ :link_text_block => lambda{|entity, text|
732
+ "pre_#{text}_post"
733
+ },
734
+ :symbol_tag => "s", :text_with_symbol_tag => "b", :username_include_symbol => true
735
+ })
736
+ expect(linked).to match(/<a[^>]+>pre_<s>#<\/s><b>hash<\/b>_post<\/a>/)
737
+ expect(linked).to match(/<a[^>]+>pre_<s>@<\/s><b>mention<\/b>_post<\/a>/)
738
+ end
739
+
740
+ it "should apply :url_target only to auto-linked URLs" do
741
+ auto_linked = @linker.auto_link("#hashtag @mention http://test.com/", {:url_target => '_blank'})
742
+ expect(auto_linked).to have_autolinked_hashtag('#hashtag')
743
+ expect(auto_linked).to link_to_screen_name('mention')
744
+ expect(auto_linked).to have_autolinked_url('http://test.com/')
745
+ expect(auto_linked).to_not match(/<a[^>]+hashtag[^>]+target[^>]+>/)
746
+ expect(auto_linked).to_not match(/<a[^>]+username[^>]+target[^>]+>/)
747
+ expect(auto_linked).to match(/<a[^>]+test.com[^>]+target=\"_blank\"[^>]*>/)
748
+ end
749
+
750
+ it "should apply target='_blank' only to auto-linked URLs when :target_blank is set to true" do
751
+ auto_linked = @linker.auto_link("#hashtag @mention http://test.com/", {:target_blank => true})
752
+ expect(auto_linked).to have_autolinked_hashtag('#hashtag')
753
+ expect(auto_linked).to link_to_screen_name('mention')
754
+ expect(auto_linked).to have_autolinked_url('http://test.com/')
755
+ expect(auto_linked).to match(/<a[^>]+hashtag[^>]+target=\"_blank\"[^>]*>/)
756
+ expect(auto_linked).to match(/<a[^>]+username[^>]+target=\"_blank\"[^>]*>/)
757
+ expect(auto_linked).to match(/<a[^>]+test.com[^>]+target=\"_blank\"[^>]*>/)
758
+ end
759
+ end
760
+
761
+ describe "link_url_with_entity" do
762
+ before do
763
+ @linker = TestAutolink.new
764
+ end
765
+
766
+ it "should use display_url and expanded_url" do
767
+ expect(@linker.send(:link_url_with_entity,
768
+ {
769
+ :url => "http://t.co/abcde",
770
+ :display_url => "twitter.com",
771
+ :expanded_url => "http://twitter.com/"},
772
+ {:invisible_tag_attrs => "class='invisible'"}).gsub('"', "'")).to be == "<span class='tco-ellipsis'><span class='invisible'>&nbsp;</span></span><span class='invisible'>http://</span><span class='js-display-url'>twitter.com</span><span class='invisible'>/</span><span class='tco-ellipsis'><span class='invisible'>&nbsp;</span></span>";
773
+ end
774
+
775
+ it "should correctly handle display_url ending with '…'" do
776
+ expect(@linker.send(:link_url_with_entity,
777
+ {
778
+ :url => "http://t.co/abcde",
779
+ :display_url => "twitter.com…",
780
+ :expanded_url => "http://twitter.com/abcdefg"},
781
+ {:invisible_tag_attrs => "class='invisible'"}).gsub('"', "'")).to be == "<span class='tco-ellipsis'><span class='invisible'>&nbsp;</span></span><span class='invisible'>http://</span><span class='js-display-url'>twitter.com</span><span class='invisible'>/abcdefg</span><span class='tco-ellipsis'><span class='invisible'>&nbsp;</span>…</span>";
782
+ end
783
+
784
+ it "should correctly handle display_url starting with '…'" do
785
+ expect(@linker.send(:link_url_with_entity,
786
+ {
787
+ :url => "http://t.co/abcde",
788
+ :display_url => "…tter.com/abcdefg",
789
+ :expanded_url => "http://twitter.com/abcdefg"},
790
+ {:invisible_tag_attrs => "class='invisible'"}).gsub('"', "'")).to be == "<span class='tco-ellipsis'>…<span class='invisible'>&nbsp;</span></span><span class='invisible'>http://twi</span><span class='js-display-url'>tter.com/abcdefg</span><span class='invisible'></span><span class='tco-ellipsis'><span class='invisible'>&nbsp;</span></span>";
791
+ end
792
+
793
+ it "should not create spans if display_url and expanded_url are on different domains" do
794
+ expect(@linker.send(:link_url_with_entity,
795
+ {
796
+ :url => "http://t.co/abcde",
797
+ :display_url => "pic.twitter.com/xyz",
798
+ :expanded_url => "http://twitter.com/foo/statuses/123/photo/1"},
799
+ {:invisible_tag_attrs => "class='invisible'"}).gsub('"', "'")).to be == "pic.twitter.com/xyz"
800
+ end
801
+ end
802
+
803
+ describe "symbol_tag" do
804
+ before do
805
+ @linker = TestAutolink.new
806
+ end
807
+ it "should put :symbol_tag around symbol" do
808
+ expect(@linker.auto_link("@mention", {:symbol_tag => 's', :username_include_symbol=>true})).to match(/<s>@<\/s>mention/)
809
+ expect(@linker.auto_link("#hash", {:symbol_tag => 's'})).to match(/<s>#<\/s>hash/)
810
+ result = @linker.auto_link("@mention #hash $CASH", {:symbol_tag => 'b', :username_include_symbol=>true})
811
+ expect(result).to match(/<b>@<\/b>mention/)
812
+ expect(result).to match(/<b>#<\/b>hash/)
813
+ expect(result).to match(/<b>\$<\/b>CASH/)
814
+ end
815
+ it "should put :text_with_symbol_tag around text" do
816
+ result = @linker.auto_link("@mention #hash $CASH", {:text_with_symbol_tag => 'b'})
817
+ expect(result).to match(/<b>mention<\/b>/)
818
+ expect(result).to match(/<b>hash<\/b>/)
819
+ expect(result).to match(/<b>CASH<\/b>/)
820
+ end
821
+ it "should put :symbol_tag around symbol and :text_with_symbol_tag around text" do
822
+ result = @linker.auto_link("@mention #hash $CASH", {:symbol_tag => 's', :text_with_symbol_tag => 'b', :username_include_symbol=>true})
823
+ expect(result).to match(/<s>@<\/s><b>mention<\/b>/)
824
+ expect(result).to match(/<s>#<\/s><b>hash<\/b>/)
825
+ expect(result).to match(/<s>\$<\/s><b>CASH<\/b>/)
826
+ end
827
+ end
828
+
829
+ describe "html_escape" do
830
+ before do
831
+ @linker = TestAutolink.new
832
+ end
833
+ it "should escape html entities properly" do
834
+ expect(@linker.html_escape("&")).to be == "&amp;"
835
+ expect(@linker.html_escape(">")).to be == "&gt;"
836
+ expect(@linker.html_escape("<")).to be == "&lt;"
837
+ expect(@linker.html_escape("\"")).to be == "&quot;"
838
+ expect(@linker.html_escape("'")).to be == "&#39;"
839
+ expect(@linker.html_escape("&<>\"")).to be == "&amp;&lt;&gt;&quot;"
840
+ expect(@linker.html_escape("<div>")).to be == "&lt;div&gt;"
841
+ expect(@linker.html_escape("a&b")).to be == "a&amp;b"
842
+ expect(@linker.html_escape("<a href=\"https://twitter.com\" target=\"_blank\">twitter & friends</a>")).to be == "&lt;a href=&quot;https://twitter.com&quot; target=&quot;_blank&quot;&gt;twitter &amp; friends&lt;/a&gt;"
843
+ expect(@linker.html_escape("&amp;")).to be == "&amp;amp;"
844
+ expect(@linker.html_escape(nil)).to be == nil
845
+ end
846
+ end
847
+
848
+ end