incollege-text 1.12.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.
@@ -0,0 +1,448 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'set'
4
+ require 'incollege-text/hash_helper'
5
+
6
+ module Incollege
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 = "post-url list-slug".freeze
12
+ # Default CSS class for auto-linked usernames
13
+ DEFAULT_USERNAME_CLASS = "post-url username".freeze
14
+ # Default CSS class for auto-linked hashtags
15
+ DEFAULT_HASHTAG_CLASS = "post-url hashtag".freeze
16
+ # Default CSS class for auto-linked cashtags
17
+ DEFAULT_CASHTAG_CLASS = "post-url cashtag".freeze
18
+
19
+ # Default URL base for auto-linked usernames
20
+ DEFAULT_USERNAME_URL_BASE = "https://incollege.com/".freeze
21
+ # Default URL base for auto-linked lists
22
+ DEFAULT_LIST_URL_BASE = "https://incollege.com/".freeze
23
+ # Default URL base for auto-linked hashtags
24
+ DEFAULT_HASHTAG_URL_BASE = "https://incollege.com/#!/search?q=%23".freeze
25
+ # Default URL base for auto-linked cashtags
26
+ DEFAULT_CASHTAG_URL_BASE = "https://incollege.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 incollege-text entity
50
+ # be careful not to alter arguments received
51
+ entities.map! do |entity|
52
+ entity = HashHelper.symbolize_keys(entity)
53
+ # hashtag
54
+ entity[:hashtag] = entity[:text] if entity[:text]
55
+ entity
56
+ end
57
+
58
+ auto_link_entities(text, entities, options)
59
+ end
60
+
61
+ def auto_link_entities(text, entities, options = {}, &block)
62
+ return text if entities.empty?
63
+
64
+ # NOTE deprecate these attributes not options keys in options hash, then use html_attrs
65
+ options = DEFAULT_OPTIONS.merge(options)
66
+ options[:html_attrs] = extract_html_attrs_from_options!(options)
67
+ options[:html_attrs][:rel] ||= "nofollow" unless options[:suppress_no_follow]
68
+ options[:html_attrs][:target] = "_blank" if options[:target_blank] == true
69
+
70
+ Incollege::Rewriter.rewrite_entities(text.dup, entities) do |entity, chars|
71
+ if entity[:url]
72
+ link_to_url(entity, chars, options, &block)
73
+ elsif entity[:hashtag]
74
+ link_to_hashtag(entity, chars, options, &block)
75
+ elsif entity[:screen_name]
76
+ link_to_screen_name(entity, chars, options, &block)
77
+ elsif entity[:cashtag]
78
+ link_to_cashtag(entity, chars, options, &block)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Add <tt><a></a></tt> tags around the usernames, lists, hashtags and URLs in the provided <tt>text</tt>.
84
+ # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash:
85
+ # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
86
+ # and place in the <tt><a></tt> tag.
87
+ #
88
+ # <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
89
+ # <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
90
+ # <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
91
+ # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
92
+ # <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
93
+ # <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.
94
+ # <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.
95
+ # <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.
96
+ # <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.
97
+ # <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
98
+ # <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
99
+ # <tt>:suppress_lists</tt>:: disable auto-linking to lists
100
+ # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
101
+ # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
102
+ # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
103
+ # <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
104
+ # <tt>:target_blank</tt>:: adds <tt>target="_blank"</tt> to all auto_linked items username / hashtag / cashtag links / urls
105
+ # <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.
106
+ # <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.
107
+ def auto_link(text, options = {}, &block)
108
+ auto_link_entities(text, Extractor.extract_entities_with_indices(text, :extract_url_without_protocol => false), options, &block)
109
+ end
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
130
+
131
+ # Add <tt><a></a></tt> tags around the hashtags in the provided <tt>text</tt>.
132
+ # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
133
+ # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
134
+ # and place in the <tt><a></tt> tag.
135
+ #
136
+ # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
137
+ # <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.
138
+ # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
139
+ # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
140
+ # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
141
+ # <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.
142
+ # <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.
143
+ def auto_link_hashtags(text, options = {}, &block) # :yields: hashtag_text
144
+ auto_link_entities(text, Extractor.extract_hashtags_with_indices(text), options, &block)
145
+ end
146
+
147
+ # Add <tt><a></a></tt> tags around the cashtags in the provided <tt>text</tt>.
148
+ # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
149
+ # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
150
+ # and place in the <tt><a></tt> tag.
151
+ #
152
+ # <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
153
+ # <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.
154
+ # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
155
+ # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
156
+ # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
157
+ # <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.
158
+ # <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.
159
+ def auto_link_cashtags(text, options = {}, &block) # :yields: cashtag_text
160
+ auto_link_entities(text, Extractor.extract_cashtags_with_indices(text), options, &block)
161
+ end
162
+
163
+ # Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
164
+ # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
165
+ # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
166
+ # and place in the <tt><a></tt> tag.
167
+ #
168
+ # <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
169
+ # <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
170
+ # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
171
+ # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
172
+ # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
173
+ # <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
174
+ # <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.
175
+ # <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.
176
+ def auto_link_urls(text, options = {}, &block)
177
+ auto_link_entities(text, Extractor.extract_urls_with_indices(text, :extract_url_without_protocol => false), options, &block)
178
+ end
179
+
180
+ # These methods are deprecated, will be removed in future.
181
+ extend Deprecation
182
+
183
+ # <b>Deprecated</b>: Please use auto_link_urls instead.
184
+ # Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
185
+ # Any elements in the <tt>href_options</tt> hash will be converted to HTML attributes
186
+ # and place in the <tt><a></tt> tag.
187
+ # Unless <tt>href_options</tt> contains <tt>:suppress_no_follow</tt>
188
+ # the <tt>rel="nofollow"</tt> attribute will be added.
189
+ alias :auto_link_urls_custom :auto_link_urls
190
+ deprecate :auto_link_urls_custom, :auto_link_urls
191
+
192
+ private
193
+
194
+ HTML_ENTITIES = {
195
+ '&' => '&amp;',
196
+ '>' => '&gt;',
197
+ '<' => '&lt;',
198
+ '"' => '&quot;',
199
+ "'" => '&#39;'
200
+ }
201
+
202
+ def html_escape(text)
203
+ text && text.to_s.gsub(/[&"'><]/) do |character|
204
+ HTML_ENTITIES[character]
205
+ end
206
+ end
207
+
208
+ # NOTE We will make this private in future.
209
+ public :html_escape
210
+
211
+ # Options which should not be passed as HTML attributes
212
+ OPTIONS_NOT_ATTRIBUTES = Set.new([
213
+ :url_class, :list_class, :username_class, :hashtag_class, :cashtag_class,
214
+ :username_url_base, :list_url_base, :hashtag_url_base, :cashtag_url_base,
215
+ :username_url_block, :list_url_block, :hashtag_url_block, :cashtag_url_block, :link_url_block,
216
+ :username_include_symbol, :suppress_lists, :suppress_no_follow, :url_entities,
217
+ :invisible_tag_attrs, :symbol_tag, :text_with_symbol_tag, :url_target, :target_blank,
218
+ :link_attribute_block, :link_text_block
219
+ ]).freeze
220
+
221
+ def extract_html_attrs_from_options!(options)
222
+ html_attrs = {}
223
+ options.reject! do |key, value|
224
+ unless OPTIONS_NOT_ATTRIBUTES.include?(key)
225
+ html_attrs[key] = value
226
+ true
227
+ end
228
+ end
229
+ html_attrs
230
+ end
231
+
232
+ def url_entities_hash(url_entities)
233
+ (url_entities || {}).inject({}) do |entities, entity|
234
+ # be careful not to alter arguments received
235
+ _entity = HashHelper.symbolize_keys(entity)
236
+ entities[_entity[:url]] = _entity
237
+ entities
238
+ end
239
+ end
240
+
241
+ def link_to_url(entity, chars, options = {})
242
+ url = entity[:url]
243
+
244
+ href = if options[:link_url_block]
245
+ options[:link_url_block].call(url)
246
+ else
247
+ url
248
+ end
249
+
250
+ # NOTE auto link to urls do not use any default values and options
251
+ # like url_class but use suppress_no_follow.
252
+ html_attrs = options[:html_attrs].dup
253
+ html_attrs[:class] = options[:url_class] if options.key?(:url_class)
254
+
255
+ # add target attribute only if :url_target is specified
256
+ html_attrs[:target] = options[:url_target] if options.key?(:url_target)
257
+
258
+ url_entities = url_entities_hash(options[:url_entities])
259
+
260
+ # use entity from urlEntities if available
261
+ url_entity = url_entities[url] || entity
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
268
+
269
+ link_to_text(entity, link_text, href, html_attrs, options)
270
+ end
271
+
272
+ def link_url_with_entity(entity, options)
273
+ display_url = entity[:display_url]
274
+ expanded_url = entity[:expanded_url]
275
+ invisible_tag_attrs = options[:invisible_tag_attrs] || DEFAULT_INVISIBLE_TAG_ATTRS
276
+
277
+ # Goal: If a user copies and pastes a post containing t.co'ed link, the resulting paste
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.incollege.com images, for which expandedUrl = "https://incollege.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.incollege.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 posts "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 &nbsp; here so that the … doesn't bump up against the URL and ruin it.
306
+ # The &nbsp; 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'>&nbsp;</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'>&nbsp;</span>
319
+ # …
320
+ # </span>
321
+ %(<span class="tco-ellipsis">#{preceding_ellipsis}<span #{invisible_tag_attrs}>&nbsp;</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}>&nbsp;</span>#{following_ellipsis}</span>)
326
+ else
327
+ html_escape(display_url)
328
+ end
329
+ end
330
+
331
+ def link_to_hashtag(entity, chars, options = {})
332
+ hash = chars[entity[:indices].first]
333
+ hashtag = entity[:hashtag]
334
+ hashtag = yield(hashtag) if block_given?
335
+ hashtag_class = options[:hashtag_class].to_s
336
+
337
+ if hashtag.match Incollege::Regex::REGEXEN[:rtl_chars]
338
+ hashtag_class += ' rtl'
339
+ end
340
+
341
+ href = if options[:hashtag_url_block]
342
+ options[:hashtag_url_block].call(hashtag)
343
+ else
344
+ "#{options[:hashtag_url_base]}#{hashtag}"
345
+ end
346
+
347
+ html_attrs = {
348
+ :class => hashtag_class,
349
+ # FIXME As our conformance test, hash in title should be half-width,
350
+ # this should be bug of conformance data.
351
+ :title => "##{hashtag}"
352
+ }.merge(options[:html_attrs])
353
+
354
+ link_to_text_with_symbol(entity, hash, hashtag, href, html_attrs, options)
355
+ end
356
+
357
+ def link_to_cashtag(entity, chars, options = {})
358
+ dollar = chars[entity[:indices].first]
359
+ cashtag = entity[:cashtag]
360
+ cashtag = yield(cashtag) if block_given?
361
+
362
+ href = if options[:cashtag_url_block]
363
+ options[:cashtag_url_block].call(cashtag)
364
+ else
365
+ "#{options[:cashtag_url_base]}#{cashtag}"
366
+ end
367
+
368
+ html_attrs = {
369
+ :class => "#{options[:cashtag_class]}",
370
+ :title => "$#{cashtag}"
371
+ }.merge(options[:html_attrs])
372
+
373
+ link_to_text_with_symbol(entity, dollar, cashtag, href, html_attrs, options)
374
+ end
375
+
376
+ def link_to_screen_name(entity, chars, options = {})
377
+ name = "#{entity[:screen_name]}#{entity[:list_slug]}"
378
+
379
+ chunk = name.dup
380
+ chunk = yield(chunk) if block_given?
381
+
382
+ at = chars[entity[:indices].first]
383
+
384
+ html_attrs = options[:html_attrs].dup
385
+
386
+ if entity[:list_slug] && !entity[:list_slug].empty? && !options[:suppress_lists]
387
+ href = if options[:list_url_block]
388
+ options[:list_url_block].call(name)
389
+ else
390
+ "#{options[:list_url_base]}#{name}"
391
+ end
392
+ html_attrs[:class] ||= "#{options[:list_class]}"
393
+ else
394
+ href = if options[:username_url_block]
395
+ options[:username_url_block].call(chunk)
396
+ else
397
+ "#{options[:username_url_base]}#{name}"
398
+ end
399
+ html_attrs[:class] ||= "#{options[:username_class]}"
400
+ end
401
+
402
+ link_to_text_with_symbol(entity, at, chunk, href, html_attrs, options)
403
+ end
404
+
405
+ def link_to_text_with_symbol(entity, symbol, text, href, attributes = {}, options = {})
406
+ tagged_symbol = options[:symbol_tag] ? "<#{options[:symbol_tag]}>#{symbol}</#{options[:symbol_tag]}>" : symbol
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 !~ Incollege::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)}"
413
+ end
414
+ end
415
+
416
+ def link_to_text(entity, text, href, attributes = {}, options = {})
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
+
425
+ def tag_attrs(attributes)
426
+ attributes.keys.sort_by{|k| k.to_s}.inject("") do |attrs, key|
427
+ value = attributes[key]
428
+
429
+ if BOOLEAN_ATTRIBUTES.include?(key)
430
+ value = value ? key : nil
431
+ end
432
+
433
+ unless value.nil?
434
+ value = case value
435
+ when Array
436
+ value.compact.join(" ")
437
+ else
438
+ value
439
+ end
440
+ attrs << %( #{html_escape(key)}="#{html_escape(value)}")
441
+ end
442
+
443
+ attrs
444
+ end
445
+ end
446
+
447
+ end
448
+ end
@@ -0,0 +1,15 @@
1
+ module Incollege
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