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