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