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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +40 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +35 -0
- data/Gemfile +4 -0
- data/LICENSE +188 -0
- data/README.md +193 -0
- data/Rakefile +52 -0
- data/config/README.md +142 -0
- data/config/v1.json +8 -0
- data/config/v2.json +29 -0
- data/config/v3.json +30 -0
- data/lib/assets/tld_lib.yml +1571 -0
- data/lib/twitter-text.rb +29 -0
- data/lib/twitter-text/autolink.rb +453 -0
- data/lib/twitter-text/configuration.rb +68 -0
- data/lib/twitter-text/deprecation.rb +21 -0
- data/lib/twitter-text/emoji_regex.rb +27 -0
- data/lib/twitter-text/extractor.rb +388 -0
- data/lib/twitter-text/hash_helper.rb +27 -0
- data/lib/twitter-text/hit_highlighter.rb +92 -0
- data/lib/twitter-text/regex.rb +381 -0
- data/lib/twitter-text/rewriter.rb +69 -0
- data/lib/twitter-text/unicode.rb +31 -0
- data/lib/twitter-text/validation.rb +251 -0
- data/lib/twitter-text/weighted_range.rb +24 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/autolinking_spec.rb +848 -0
- data/spec/configuration_spec.rb +136 -0
- data/spec/extractor_spec.rb +392 -0
- data/spec/hithighlighter_spec.rb +96 -0
- data/spec/regex_spec.rb +76 -0
- data/spec/rewriter_spec.rb +553 -0
- data/spec/spec_helper.rb +139 -0
- data/spec/test_urls.rb +90 -0
- data/spec/twitter_text_spec.rb +25 -0
- data/spec/unicode_spec.rb +35 -0
- data/spec/validation_spec.rb +87 -0
- data/test/conformance_test.rb +242 -0
- data/twitter-text.gemspec +35 -0
- metadata +229 -0
@@ -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
|
data/script/destroy
ADDED
@@ -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)
|
data/script/generate
ADDED
@@ -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'> </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'> </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'> </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'> </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'> </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'> </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 == "&"
|
835
|
+
expect(@linker.html_escape(">")).to be == ">"
|
836
|
+
expect(@linker.html_escape("<")).to be == "<"
|
837
|
+
expect(@linker.html_escape("\"")).to be == """
|
838
|
+
expect(@linker.html_escape("'")).to be == "'"
|
839
|
+
expect(@linker.html_escape("&<>\"")).to be == "&<>""
|
840
|
+
expect(@linker.html_escape("<div>")).to be == "<div>"
|
841
|
+
expect(@linker.html_escape("a&b")).to be == "a&b"
|
842
|
+
expect(@linker.html_escape("<a href=\"https://twitter.com\" target=\"_blank\">twitter & friends</a>")).to be == "<a href="https://twitter.com" target="_blank">twitter & friends</a>"
|
843
|
+
expect(@linker.html_escape("&")).to be == "&amp;"
|
844
|
+
expect(@linker.html_escape(nil)).to be == nil
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
end
|