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