twitter-text-editted 1.0.0

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