ritter 0.0.87 → 0.0.88

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