twitter-text 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +5 -5
- data/lib/twitter-text/autolink.rb +386 -385
- data/lib/twitter-text/configuration.rb +48 -47
- data/lib/twitter-text/deprecation.rb +11 -9
- data/lib/twitter-text/extractor.rb +270 -268
- data/lib/twitter-text/hash_helper.rb +17 -15
- data/lib/twitter-text/hit_highlighter.rb +69 -67
- data/lib/twitter-text/regex.rb +342 -340
- data/lib/twitter-text/rewriter.rb +51 -49
- data/lib/twitter-text/unicode.rb +21 -20
- data/lib/twitter-text/validation.rb +185 -183
- data/lib/twitter-text/weighted_range.rb +12 -10
- data/spec/autolinking_spec.rb +2 -2
- data/spec/configuration_spec.rb +11 -11
- data/spec/extractor_spec.rb +6 -6
- data/spec/hithighlighter_spec.rb +2 -2
- data/spec/regex_spec.rb +3 -3
- data/spec/rewriter_spec.rb +7 -7
- data/spec/spec_helper.rb +2 -2
- data/spec/unicode_spec.rb +11 -11
- data/spec/validation_spec.rb +7 -7
- data/test/conformance_test.rb +4 -4
- data/twitter-text.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f7622cf10e3345995a426a4e371acef2e6eaa7344c430d9c5f944ba9f822d98
|
4
|
+
data.tar.gz: 1c70f9348e8a801f1df6eaeac98f98c7c2cca685354cc06181ac081acdcfd304
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ab02e8044a6d8fd1c25dc70a53d5b981b37c3b5c1458d193c090e401a4822349c99463946827101d27ee4156bcf98597be54990026145c820dedecda1d9b675
|
7
|
+
data.tar.gz: b3163606bd143c4d13efbc1aab07cea05ad03d2e26f319c053662a758a44f990d132a3579fa5a0cc8f0023cdfdcf3584fb1bfe23d63c3636b276c02efdb38049
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
## [Unreleased]
|
5
|
+
|
6
|
+
## [2.1] - 2017-12-20
|
7
|
+
### Added
|
8
|
+
- This CHANGELOG.md file
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
- Top-level namespace changed from `Twitter` to `Twitter::TwitterText`. This
|
12
|
+
resolves a namespace collision with the popular
|
13
|
+
[twitter gem](https://github.com/sferik/twitter). This is considered
|
14
|
+
a breaking change, so the version has been bumped to 2.1. This fixes
|
15
|
+
issue [#221](https://github.com/twitter/twitter-text/issues/221),
|
16
|
+
"NoMethodError Exception: undefined method `[]' for nil:NilClasswhen
|
17
|
+
using gem in rails app"
|
18
|
+
|
19
|
+
## [2.0.2] - 2017-12-18
|
20
|
+
### Changed
|
21
|
+
- Resolved issue
|
22
|
+
[#211](https://github.com/twitter/twitter-text/issues/211), "gem
|
23
|
+
breaks, asset file is a dangling symlink"
|
24
|
+
- config files, tld_lib.yml files now copied into the right place
|
25
|
+
- Rakefile now included `prebuild`, `clean` tasks
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ def parse_tweet(text, options = {}) { ... }
|
|
50
50
|
|
51
51
|
This method takes a string as input and returns a results object that
|
52
52
|
contains information about the
|
53
|
-
string. `Twitter::Validation::ParseResults` object includes:
|
53
|
+
string. `Twitter::TwitterText::Validation::ParseResults` object includes:
|
54
54
|
|
55
55
|
* `:weighted_length`: the overall length of the tweet with code points
|
56
56
|
weighted per the ranges defined in the configuration file.
|
@@ -78,7 +78,7 @@ payload see [Tweet updates](https://developer.twitter.com/en/docs/tweets/tweet-u
|
|
78
78
|
# Extraction
|
79
79
|
```ruby
|
80
80
|
class MyClass
|
81
|
-
include Twitter::Extractor
|
81
|
+
include Twitter::TwitterText::Extractor
|
82
82
|
usernames = extract_mentioned_screen_names("Mentioning @twitter and @jack")
|
83
83
|
# usernames = ["twitter", "jack"]
|
84
84
|
end
|
@@ -88,7 +88,7 @@ end
|
|
88
88
|
|
89
89
|
```ruby
|
90
90
|
class MyClass
|
91
|
-
include Twitter::Extractor
|
91
|
+
include Twitter::TwitterText::Extractor
|
92
92
|
extract_reply_screen_name("@twitter are you hiring?").do |username|
|
93
93
|
# username = "twitter"
|
94
94
|
end
|
@@ -101,7 +101,7 @@ end
|
|
101
101
|
|
102
102
|
```ruby
|
103
103
|
class MyClass
|
104
|
-
include Twitter::Autolink
|
104
|
+
include Twitter::TwitterText::Autolink
|
105
105
|
|
106
106
|
html = auto_link("link @user, please #request")
|
107
107
|
end
|
@@ -110,7 +110,7 @@ end
|
|
110
110
|
### For Ruby on Rails you want to add this to app/helpers/application_helper.rb
|
111
111
|
```ruby
|
112
112
|
module ApplicationHelper
|
113
|
-
include Twitter::Autolink
|
113
|
+
include Twitter::TwitterText::Autolink
|
114
114
|
end
|
115
115
|
```
|
116
116
|
|
@@ -4,445 +4,446 @@ require 'set'
|
|
4
4
|
require 'twitter-text/hash_helper'
|
5
5
|
|
6
6
|
module Twitter
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
7
|
+
module TwitterText
|
8
|
+
# A module for including Tweet auto-linking in a class. The primary use of this is for helpers/views so they can auto-link
|
9
|
+
# usernames, lists, hashtags and URLs.
|
10
|
+
module Autolink extend self
|
11
|
+
# Default CSS class for auto-linked lists
|
12
|
+
DEFAULT_LIST_CLASS = "tweet-url list-slug".freeze
|
13
|
+
# Default CSS class for auto-linked usernames
|
14
|
+
DEFAULT_USERNAME_CLASS = "tweet-url username".freeze
|
15
|
+
# Default CSS class for auto-linked hashtags
|
16
|
+
DEFAULT_HASHTAG_CLASS = "tweet-url hashtag".freeze
|
17
|
+
# Default CSS class for auto-linked cashtags
|
18
|
+
DEFAULT_CASHTAG_CLASS = "tweet-url cashtag".freeze
|
19
|
+
|
20
|
+
# Default URL base for auto-linked usernames
|
21
|
+
DEFAULT_USERNAME_URL_BASE = "https://twitter.com/".freeze
|
22
|
+
# Default URL base for auto-linked lists
|
23
|
+
DEFAULT_LIST_URL_BASE = "https://twitter.com/".freeze
|
24
|
+
# Default URL base for auto-linked hashtags
|
25
|
+
DEFAULT_HASHTAG_URL_BASE = "https://twitter.com/search?q=%23".freeze
|
26
|
+
# Default URL base for auto-linked cashtags
|
27
|
+
DEFAULT_CASHTAG_URL_BASE = "https://twitter.com/search?q=%24".freeze
|
28
|
+
|
29
|
+
# Default attributes for invisible span tag
|
30
|
+
DEFAULT_INVISIBLE_TAG_ATTRS = "style='position:absolute;left:-9999px;'".freeze
|
31
|
+
|
32
|
+
DEFAULT_OPTIONS = {
|
33
|
+
:list_class => DEFAULT_LIST_CLASS,
|
34
|
+
:username_class => DEFAULT_USERNAME_CLASS,
|
35
|
+
:hashtag_class => DEFAULT_HASHTAG_CLASS,
|
36
|
+
:cashtag_class => DEFAULT_CASHTAG_CLASS,
|
37
|
+
|
38
|
+
:username_url_base => DEFAULT_USERNAME_URL_BASE,
|
39
|
+
:list_url_base => DEFAULT_LIST_URL_BASE,
|
40
|
+
:hashtag_url_base => DEFAULT_HASHTAG_URL_BASE,
|
41
|
+
:cashtag_url_base => DEFAULT_CASHTAG_URL_BASE,
|
42
|
+
|
43
|
+
:invisible_tag_attrs => DEFAULT_INVISIBLE_TAG_ATTRS
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
def auto_link_with_json(text, json, options = {})
|
47
|
+
# concatenate entities
|
48
|
+
entities = json.values().flatten()
|
49
|
+
|
50
|
+
# map JSON entity to twitter-text entity
|
51
|
+
# be careful not to alter arguments received
|
52
|
+
entities.map! do |entity|
|
53
|
+
entity = HashHelper.symbolize_keys(entity)
|
54
|
+
# hashtag
|
55
|
+
entity[:hashtag] = entity[:text] if entity[:text]
|
56
|
+
entity
|
57
|
+
end
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
auto_link_entities(text, entities, options)
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
62
|
+
def auto_link_entities(text, entities, options = {}, &block)
|
63
|
+
return text if entities.empty?
|
64
|
+
|
65
|
+
# NOTE deprecate these attributes not options keys in options hash, then use html_attrs
|
66
|
+
options = DEFAULT_OPTIONS.merge(options)
|
67
|
+
options[:html_attrs] = extract_html_attrs_from_options!(options)
|
68
|
+
options[:html_attrs][:rel] ||= "nofollow" unless options[:suppress_no_follow]
|
69
|
+
options[:html_attrs][:target] = "_blank" if options[:target_blank] == true
|
70
|
+
|
71
|
+
Twitter::TwitterText::Rewriter.rewrite_entities(text.dup, entities) do |entity, chars|
|
72
|
+
if entity[:url]
|
73
|
+
link_to_url(entity, chars, options, &block)
|
74
|
+
elsif entity[:hashtag]
|
75
|
+
link_to_hashtag(entity, chars, options, &block)
|
76
|
+
elsif entity[:screen_name]
|
77
|
+
link_to_screen_name(entity, chars, options, &block)
|
78
|
+
elsif entity[:cashtag]
|
79
|
+
link_to_cashtag(entity, chars, options, &block)
|
80
|
+
end
|
79
81
|
end
|
80
82
|
end
|
81
|
-
end
|
82
83
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
# Add <tt><a></a></tt> tags around the usernames and lists in the provided <tt>text</tt>. The
|
112
|
-
# <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
|
113
|
-
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
114
|
-
# and place in the <tt><a></tt> tag.
|
115
|
-
#
|
116
|
-
# <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
|
117
|
-
# <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
|
118
|
-
# <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.
|
119
|
-
# <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.
|
120
|
-
# <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
|
121
|
-
# <tt>:suppress_lists</tt>:: disable auto-linking to lists
|
122
|
-
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
123
|
-
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
124
|
-
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
125
|
-
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
126
|
-
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
127
|
-
def auto_link_usernames_or_lists(text, options = {}, &block) # :yields: list_or_username
|
128
|
-
auto_link_entities(text, Extractor.extract_mentions_or_lists_with_indices(text), options, &block)
|
129
|
-
end
|
84
|
+
# Add <tt><a></a></tt> tags around the usernames, lists, hashtags and URLs in the provided <tt>text</tt>.
|
85
|
+
# The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash:
|
86
|
+
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
87
|
+
# and place in the <tt><a></tt> tag.
|
88
|
+
#
|
89
|
+
# <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
|
90
|
+
# <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
|
91
|
+
# <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
|
92
|
+
# <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
|
93
|
+
# <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
|
94
|
+
# <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.
|
95
|
+
# <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.
|
96
|
+
# <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.
|
97
|
+
# <tt>:cashtag_url_base</tt>:: the value for <tt>href</tt> attribute on cashtag links. The <tt>$cashtag</tt> (minus the <tt>$</tt>) will be appended at the end of this.
|
98
|
+
# <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
|
99
|
+
# <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
|
100
|
+
# <tt>:suppress_lists</tt>:: disable auto-linking to lists
|
101
|
+
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
102
|
+
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
103
|
+
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
104
|
+
# <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
|
105
|
+
# <tt>:target_blank</tt>:: adds <tt>target="_blank"</tt> to all auto_linked items username / hashtag / cashtag links / urls
|
106
|
+
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
107
|
+
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
108
|
+
def auto_link(text, options = {}, &block)
|
109
|
+
auto_link_entities(text, Extractor.extract_entities_with_indices(text, :extract_url_without_protocol => false), options, &block)
|
110
|
+
end
|
130
111
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
112
|
+
# Add <tt><a></a></tt> tags around the usernames and lists in the provided <tt>text</tt>. The
|
113
|
+
# <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
|
114
|
+
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
115
|
+
# and place in the <tt><a></tt> tag.
|
116
|
+
#
|
117
|
+
# <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
|
118
|
+
# <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
|
119
|
+
# <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.
|
120
|
+
# <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.
|
121
|
+
# <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
|
122
|
+
# <tt>:suppress_lists</tt>:: disable auto-linking to lists
|
123
|
+
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
124
|
+
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
125
|
+
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
126
|
+
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
127
|
+
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
128
|
+
def auto_link_usernames_or_lists(text, options = {}, &block) # :yields: list_or_username
|
129
|
+
auto_link_entities(text, Extractor.extract_mentions_or_lists_with_indices(text), options, &block)
|
130
|
+
end
|
146
131
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
132
|
+
# Add <tt><a></a></tt> tags around the hashtags in the provided <tt>text</tt>.
|
133
|
+
# The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
|
134
|
+
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
135
|
+
# and place in the <tt><a></tt> tag.
|
136
|
+
#
|
137
|
+
# <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
|
138
|
+
# <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.
|
139
|
+
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
140
|
+
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
141
|
+
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
142
|
+
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
143
|
+
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
144
|
+
def auto_link_hashtags(text, options = {}, &block) # :yields: hashtag_text
|
145
|
+
auto_link_entities(text, Extractor.extract_hashtags_with_indices(text), options, &block)
|
146
|
+
end
|
162
147
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
end
|
148
|
+
# Add <tt><a></a></tt> tags around the cashtags in the provided <tt>text</tt>.
|
149
|
+
# The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
|
150
|
+
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
151
|
+
# and place in the <tt><a></tt> tag.
|
152
|
+
#
|
153
|
+
# <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
|
154
|
+
# <tt>:cashtag_url_base</tt>:: the value for <tt>href</tt> attribute. The cashtag text (minus the <tt>$</tt>) will be appended at the end of this.
|
155
|
+
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
156
|
+
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
157
|
+
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
158
|
+
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
159
|
+
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
160
|
+
def auto_link_cashtags(text, options = {}, &block) # :yields: cashtag_text
|
161
|
+
auto_link_entities(text, Extractor.extract_cashtags_with_indices(text), options, &block)
|
162
|
+
end
|
179
163
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
'&' => '&',
|
196
|
-
'>' => '>',
|
197
|
-
'<' => '<',
|
198
|
-
'"' => '"',
|
199
|
-
"'" => '''
|
200
|
-
}
|
201
|
-
|
202
|
-
def html_escape(text)
|
203
|
-
text && text.to_s.gsub(/[&"'><]/) do |character|
|
204
|
-
HTML_ENTITIES[character]
|
164
|
+
# Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
|
165
|
+
# The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
|
166
|
+
# Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
|
167
|
+
# and place in the <tt><a></tt> tag.
|
168
|
+
#
|
169
|
+
# <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
|
170
|
+
# <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
|
171
|
+
# <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
|
172
|
+
# <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
|
173
|
+
# <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
|
174
|
+
# <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
|
175
|
+
# <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
|
176
|
+
# <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
|
177
|
+
def auto_link_urls(text, options = {}, &block)
|
178
|
+
auto_link_entities(text, Extractor.extract_urls_with_indices(text, :extract_url_without_protocol => false), options, &block)
|
205
179
|
end
|
206
|
-
end
|
207
180
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
:
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
181
|
+
# These methods are deprecated, will be removed in future.
|
182
|
+
extend Deprecation
|
183
|
+
|
184
|
+
# <b>Deprecated</b>: Please use auto_link_urls instead.
|
185
|
+
# Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
|
186
|
+
# Any elements in the <tt>href_options</tt> hash will be converted to HTML attributes
|
187
|
+
# and place in the <tt><a></tt> tag.
|
188
|
+
# Unless <tt>href_options</tt> contains <tt>:suppress_no_follow</tt>
|
189
|
+
# the <tt>rel="nofollow"</tt> attribute will be added.
|
190
|
+
alias :auto_link_urls_custom :auto_link_urls
|
191
|
+
deprecate :auto_link_urls_custom, :auto_link_urls
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
HTML_ENTITIES = {
|
196
|
+
'&' => '&',
|
197
|
+
'>' => '>',
|
198
|
+
'<' => '<',
|
199
|
+
'"' => '"',
|
200
|
+
"'" => '''
|
201
|
+
}
|
202
|
+
|
203
|
+
def html_escape(text)
|
204
|
+
text && text.to_s.gsub(/[&"'><]/) do |character|
|
205
|
+
HTML_ENTITIES[character]
|
227
206
|
end
|
228
207
|
end
|
229
|
-
html_attrs
|
230
|
-
end
|
231
208
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
209
|
+
# NOTE We will make this private in future.
|
210
|
+
public :html_escape
|
211
|
+
|
212
|
+
# Options which should not be passed as HTML attributes
|
213
|
+
OPTIONS_NOT_ATTRIBUTES = Set.new([
|
214
|
+
:url_class, :list_class, :username_class, :hashtag_class, :cashtag_class,
|
215
|
+
:username_url_base, :list_url_base, :hashtag_url_base, :cashtag_url_base,
|
216
|
+
:username_url_block, :list_url_block, :hashtag_url_block, :cashtag_url_block, :link_url_block,
|
217
|
+
:username_include_symbol, :suppress_lists, :suppress_no_follow, :url_entities,
|
218
|
+
:invisible_tag_attrs, :symbol_tag, :text_with_symbol_tag, :url_target, :target_blank,
|
219
|
+
:link_attribute_block, :link_text_block
|
220
|
+
]).freeze
|
221
|
+
|
222
|
+
def extract_html_attrs_from_options!(options)
|
223
|
+
html_attrs = {}
|
224
|
+
options.reject! do |key, value|
|
225
|
+
unless OPTIONS_NOT_ATTRIBUTES.include?(key)
|
226
|
+
html_attrs[key] = value
|
227
|
+
true
|
228
|
+
end
|
229
|
+
end
|
230
|
+
html_attrs
|
238
231
|
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def link_to_url(entity, chars, options = {})
|
242
|
-
url = entity[:url]
|
243
232
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
233
|
+
def url_entities_hash(url_entities)
|
234
|
+
(url_entities || {}).inject({}) do |entities, entity|
|
235
|
+
# be careful not to alter arguments received
|
236
|
+
_entity = HashHelper.symbolize_keys(entity)
|
237
|
+
entities[_entity[:url]] = _entity
|
238
|
+
entities
|
239
|
+
end
|
248
240
|
end
|
249
241
|
|
250
|
-
|
251
|
-
|
252
|
-
html_attrs = options[:html_attrs].dup
|
253
|
-
html_attrs[:class] = options[:url_class] if options.key?(:url_class)
|
242
|
+
def link_to_url(entity, chars, options = {})
|
243
|
+
url = entity[:url]
|
254
244
|
|
255
|
-
|
256
|
-
|
245
|
+
href = if options[:link_url_block]
|
246
|
+
options[:link_url_block].call(url)
|
247
|
+
else
|
248
|
+
url
|
249
|
+
end
|
257
250
|
|
258
|
-
|
251
|
+
# NOTE auto link to urls do not use any default values and options
|
252
|
+
# like url_class but use suppress_no_follow.
|
253
|
+
html_attrs = options[:html_attrs].dup
|
254
|
+
html_attrs[:class] = options[:url_class] if options.key?(:url_class)
|
259
255
|
|
260
|
-
|
261
|
-
|
262
|
-
link_text = if url_entity[:display_url]
|
263
|
-
html_attrs[:title] ||= url_entity[:expanded_url]
|
264
|
-
link_url_with_entity(url_entity, options)
|
265
|
-
else
|
266
|
-
html_escape(url)
|
267
|
-
end
|
256
|
+
# add target attribute only if :url_target is specified
|
257
|
+
html_attrs[:target] = options[:url_target] if options.key?(:url_target)
|
268
258
|
|
269
|
-
|
270
|
-
end
|
259
|
+
url_entities = url_entities_hash(options[:url_entities])
|
271
260
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
261
|
+
# use entity from urlEntities if available
|
262
|
+
url_entity = url_entities[url] || entity
|
263
|
+
link_text = if url_entity[:display_url]
|
264
|
+
html_attrs[:title] ||= url_entity[:expanded_url]
|
265
|
+
link_url_with_entity(url_entity, options)
|
266
|
+
else
|
267
|
+
html_escape(url)
|
268
|
+
end
|
276
269
|
|
277
|
-
|
278
|
-
# should contain the full original URL (expanded_url), not the display URL.
|
279
|
-
#
|
280
|
-
# Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
|
281
|
-
# font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
|
282
|
-
# Elements with font-size:0 get copied even though they are not visible.
|
283
|
-
# Note that display:none doesn't work here. Elements with display:none don't get copied.
|
284
|
-
#
|
285
|
-
# Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
|
286
|
-
# wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
|
287
|
-
# everything with the tco-ellipsis class.
|
288
|
-
#
|
289
|
-
# Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/username/status/1234/photo/1
|
290
|
-
# For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
|
291
|
-
# For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
|
292
|
-
display_url_sans_ellipses = display_url.gsub("…", "")
|
293
|
-
|
294
|
-
if expanded_url.include?(display_url_sans_ellipses)
|
295
|
-
before_display_url, after_display_url = expanded_url.split(display_url_sans_ellipses, 2)
|
296
|
-
preceding_ellipsis = /\A…/.match(display_url).to_s
|
297
|
-
following_ellipsis = /…\z/.match(display_url).to_s
|
298
|
-
|
299
|
-
# As an example: The user tweets "hi http://longdomainname.com/foo"
|
300
|
-
# This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
|
301
|
-
# This will get rendered as:
|
302
|
-
# <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
303
|
-
# …
|
304
|
-
# <!-- There's a chance the onCopy event handler might not fire. In case that happens,
|
305
|
-
# we include an here so that the … doesn't bump up against the URL and ruin it.
|
306
|
-
# The is inside the tco-ellipsis span so that when the onCopy handler *does*
|
307
|
-
# fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
|
308
|
-
# e.g. "hi http://longdomainname.com/foo".
|
309
|
-
# <span style='font-size:0'> </span>
|
310
|
-
# </span>
|
311
|
-
# <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
|
312
|
-
# http://longdomai
|
313
|
-
# </span>
|
314
|
-
# <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
|
315
|
-
# nname.com/foo
|
316
|
-
# </span>
|
317
|
-
# <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
318
|
-
# <span style='font-size:0'> </span>
|
319
|
-
# …
|
320
|
-
# </span>
|
321
|
-
%(<span class="tco-ellipsis">#{preceding_ellipsis}<span #{invisible_tag_attrs}> </span></span>) <<
|
322
|
-
%(<span #{invisible_tag_attrs}>#{html_escape(before_display_url)}</span>) <<
|
323
|
-
%(<span class="js-display-url">#{html_escape(display_url_sans_ellipses)}</span>) <<
|
324
|
-
%(<span #{invisible_tag_attrs}>#{html_escape(after_display_url)}</span>) <<
|
325
|
-
%(<span class="tco-ellipsis"><span #{invisible_tag_attrs}> </span>#{following_ellipsis}</span>)
|
326
|
-
else
|
327
|
-
html_escape(display_url)
|
270
|
+
link_to_text(entity, link_text, href, html_attrs, options)
|
328
271
|
end
|
329
|
-
end
|
330
272
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
273
|
+
def link_url_with_entity(entity, options)
|
274
|
+
display_url = entity[:display_url]
|
275
|
+
expanded_url = entity[:expanded_url]
|
276
|
+
invisible_tag_attrs = options[:invisible_tag_attrs] || DEFAULT_INVISIBLE_TAG_ATTRS
|
277
|
+
|
278
|
+
# Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
|
279
|
+
# should contain the full original URL (expanded_url), not the display URL.
|
280
|
+
#
|
281
|
+
# Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
|
282
|
+
# font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
|
283
|
+
# Elements with font-size:0 get copied even though they are not visible.
|
284
|
+
# Note that display:none doesn't work here. Elements with display:none don't get copied.
|
285
|
+
#
|
286
|
+
# Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
|
287
|
+
# wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
|
288
|
+
# everything with the tco-ellipsis class.
|
289
|
+
#
|
290
|
+
# Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/username/status/1234/photo/1
|
291
|
+
# For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
|
292
|
+
# For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
|
293
|
+
display_url_sans_ellipses = display_url.gsub("…", "")
|
294
|
+
|
295
|
+
if expanded_url.include?(display_url_sans_ellipses)
|
296
|
+
before_display_url, after_display_url = expanded_url.split(display_url_sans_ellipses, 2)
|
297
|
+
preceding_ellipsis = /\A…/.match(display_url).to_s
|
298
|
+
following_ellipsis = /…\z/.match(display_url).to_s
|
299
|
+
|
300
|
+
# As an example: The user tweets "hi http://longdomainname.com/foo"
|
301
|
+
# This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
|
302
|
+
# This will get rendered as:
|
303
|
+
# <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
304
|
+
# …
|
305
|
+
# <!-- There's a chance the onCopy event handler might not fire. In case that happens,
|
306
|
+
# we include an here so that the … doesn't bump up against the URL and ruin it.
|
307
|
+
# The is inside the tco-ellipsis span so that when the onCopy handler *does*
|
308
|
+
# fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
|
309
|
+
# e.g. "hi http://longdomainname.com/foo".
|
310
|
+
# <span style='font-size:0'> </span>
|
311
|
+
# </span>
|
312
|
+
# <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
|
313
|
+
# http://longdomai
|
314
|
+
# </span>
|
315
|
+
# <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
|
316
|
+
# nname.com/foo
|
317
|
+
# </span>
|
318
|
+
# <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
319
|
+
# <span style='font-size:0'> </span>
|
320
|
+
# …
|
321
|
+
# </span>
|
322
|
+
%(<span class="tco-ellipsis">#{preceding_ellipsis}<span #{invisible_tag_attrs}> </span></span>) <<
|
323
|
+
%(<span #{invisible_tag_attrs}>#{html_escape(before_display_url)}</span>) <<
|
324
|
+
%(<span class="js-display-url">#{html_escape(display_url_sans_ellipses)}</span>) <<
|
325
|
+
%(<span #{invisible_tag_attrs}>#{html_escape(after_display_url)}</span>) <<
|
326
|
+
%(<span class="tco-ellipsis"><span #{invisible_tag_attrs}> </span>#{following_ellipsis}</span>)
|
327
|
+
else
|
328
|
+
html_escape(display_url)
|
329
|
+
end
|
339
330
|
end
|
340
331
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
332
|
+
def link_to_hashtag(entity, chars, options = {})
|
333
|
+
hash = chars[entity[:indices].first]
|
334
|
+
hashtag = entity[:hashtag]
|
335
|
+
hashtag = yield(hashtag) if block_given?
|
336
|
+
hashtag_class = options[:hashtag_class].to_s
|
346
337
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
# this should be bug of conformance data.
|
351
|
-
:title => "##{hashtag}"
|
352
|
-
}.merge(options[:html_attrs])
|
338
|
+
if hashtag.match Twitter::TwitterText::Regex::REGEXEN[:rtl_chars]
|
339
|
+
hashtag_class += ' rtl'
|
340
|
+
end
|
353
341
|
|
354
|
-
|
355
|
-
|
342
|
+
href = if options[:hashtag_url_block]
|
343
|
+
options[:hashtag_url_block].call(hashtag)
|
344
|
+
else
|
345
|
+
"#{options[:hashtag_url_base]}#{hashtag}"
|
346
|
+
end
|
356
347
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
348
|
+
html_attrs = {
|
349
|
+
:class => hashtag_class,
|
350
|
+
# FIXME As our conformance test, hash in title should be half-width,
|
351
|
+
# this should be bug of conformance data.
|
352
|
+
:title => "##{hashtag}"
|
353
|
+
}.merge(options[:html_attrs])
|
361
354
|
|
362
|
-
|
363
|
-
options[:cashtag_url_block].call(cashtag)
|
364
|
-
else
|
365
|
-
"#{options[:cashtag_url_base]}#{cashtag}"
|
355
|
+
link_to_text_with_symbol(entity, hash, hashtag, href, html_attrs, options)
|
366
356
|
end
|
367
357
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
358
|
+
def link_to_cashtag(entity, chars, options = {})
|
359
|
+
dollar = chars[entity[:indices].first]
|
360
|
+
cashtag = entity[:cashtag]
|
361
|
+
cashtag = yield(cashtag) if block_given?
|
372
362
|
|
373
|
-
|
374
|
-
|
363
|
+
href = if options[:cashtag_url_block]
|
364
|
+
options[:cashtag_url_block].call(cashtag)
|
365
|
+
else
|
366
|
+
"#{options[:cashtag_url_base]}#{cashtag}"
|
367
|
+
end
|
368
|
+
|
369
|
+
html_attrs = {
|
370
|
+
:class => "#{options[:cashtag_class]}",
|
371
|
+
:title => "$#{cashtag}"
|
372
|
+
}.merge(options[:html_attrs])
|
375
373
|
|
376
|
-
|
377
|
-
|
374
|
+
link_to_text_with_symbol(entity, dollar, cashtag, href, html_attrs, options)
|
375
|
+
end
|
378
376
|
|
379
|
-
|
380
|
-
|
377
|
+
def link_to_screen_name(entity, chars, options = {})
|
378
|
+
name = "#{entity[:screen_name]}#{entity[:list_slug]}"
|
381
379
|
|
382
|
-
|
380
|
+
chunk = name.dup
|
381
|
+
chunk = yield(chunk) if block_given?
|
383
382
|
|
384
|
-
|
383
|
+
at = chars[entity[:indices].first]
|
385
384
|
|
386
|
-
|
387
|
-
|
388
|
-
|
385
|
+
html_attrs = options[:html_attrs].dup
|
386
|
+
|
387
|
+
if entity[:list_slug] && !entity[:list_slug].empty? && !options[:suppress_lists]
|
388
|
+
href = if options[:list_url_block]
|
389
|
+
options[:list_url_block].call(name)
|
390
|
+
else
|
391
|
+
"#{options[:list_url_base]}#{name}"
|
392
|
+
end
|
393
|
+
html_attrs[:class] ||= "#{options[:list_class]}"
|
389
394
|
else
|
390
|
-
|
395
|
+
href = if options[:username_url_block]
|
396
|
+
options[:username_url_block].call(chunk)
|
397
|
+
else
|
398
|
+
"#{options[:username_url_base]}#{name}"
|
399
|
+
end
|
400
|
+
html_attrs[:class] ||= "#{options[:username_class]}"
|
391
401
|
end
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
402
|
+
|
403
|
+
link_to_text_with_symbol(entity, at, chunk, href, html_attrs, options)
|
404
|
+
end
|
405
|
+
|
406
|
+
def link_to_text_with_symbol(entity, symbol, text, href, attributes = {}, options = {})
|
407
|
+
tagged_symbol = options[:symbol_tag] ? "<#{options[:symbol_tag]}>#{symbol}</#{options[:symbol_tag]}>" : symbol
|
408
|
+
text = html_escape(text)
|
409
|
+
tagged_text = options[:text_with_symbol_tag] ? "<#{options[:text_with_symbol_tag]}>#{text}</#{options[:text_with_symbol_tag]}>" : text
|
410
|
+
if options[:username_include_symbol] || symbol !~ Twitter::TwitterText::Regex::REGEXEN[:at_signs]
|
411
|
+
"#{link_to_text(entity, tagged_symbol + tagged_text, href, attributes, options)}"
|
396
412
|
else
|
397
|
-
"#{
|
413
|
+
"#{tagged_symbol}#{link_to_text(entity, tagged_text, href, attributes, options)}"
|
398
414
|
end
|
399
|
-
html_attrs[:class] ||= "#{options[:username_class]}"
|
400
415
|
end
|
401
416
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
text = html_escape(text)
|
408
|
-
tagged_text = options[:text_with_symbol_tag] ? "<#{options[:text_with_symbol_tag]}>#{text}</#{options[:text_with_symbol_tag]}>" : text
|
409
|
-
if options[:username_include_symbol] || symbol !~ Twitter::Regex::REGEXEN[:at_signs]
|
410
|
-
"#{link_to_text(entity, tagged_symbol + tagged_text, href, attributes, options)}"
|
411
|
-
else
|
412
|
-
"#{tagged_symbol}#{link_to_text(entity, tagged_text, href, attributes, options)}"
|
417
|
+
def link_to_text(entity, text, href, attributes = {}, options = {})
|
418
|
+
attributes[:href] = href
|
419
|
+
options[:link_attribute_block].call(entity, attributes) if options[:link_attribute_block]
|
420
|
+
text = options[:link_text_block].call(entity, text) if options[:link_text_block]
|
421
|
+
%(<a#{tag_attrs(attributes)}>#{text}</a>)
|
413
422
|
end
|
414
|
-
end
|
415
423
|
|
416
|
-
|
417
|
-
attributes[:href] = href
|
418
|
-
options[:link_attribute_block].call(entity, attributes) if options[:link_attribute_block]
|
419
|
-
text = options[:link_text_block].call(entity, text) if options[:link_text_block]
|
420
|
-
%(<a#{tag_attrs(attributes)}>#{text}</a>)
|
421
|
-
end
|
422
|
-
|
423
|
-
BOOLEAN_ATTRIBUTES = Set.new([:disabled, :readonly, :multiple, :checked]).freeze
|
424
|
+
BOOLEAN_ATTRIBUTES = Set.new([:disabled, :readonly, :multiple, :checked]).freeze
|
424
425
|
|
425
|
-
|
426
|
-
|
427
|
-
|
426
|
+
def tag_attrs(attributes)
|
427
|
+
attributes.keys.sort_by{|k| k.to_s}.inject("") do |attrs, key|
|
428
|
+
value = attributes[key]
|
428
429
|
|
429
|
-
|
430
|
-
|
431
|
-
|
430
|
+
if BOOLEAN_ATTRIBUTES.include?(key)
|
431
|
+
value = value ? key : nil
|
432
|
+
end
|
432
433
|
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
434
|
+
unless value.nil?
|
435
|
+
value = case value
|
436
|
+
when Array
|
437
|
+
value.compact.join(" ")
|
438
|
+
else
|
439
|
+
value
|
440
|
+
end
|
441
|
+
attrs << %( #{html_escape(key)}="#{html_escape(value)}")
|
439
442
|
end
|
440
|
-
attrs << %( #{html_escape(key)}="#{html_escape(value)}")
|
441
|
-
end
|
442
443
|
|
443
|
-
|
444
|
+
attrs
|
445
|
+
end
|
444
446
|
end
|
445
447
|
end
|
446
|
-
|
447
448
|
end
|
448
449
|
end
|