PageRenderTime 26ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/actionpack/lib/action_view/helpers/text_helper.rb

https://github.com/ghar/rails
Ruby | 417 lines | 135 code | 31 blank | 251 comment | 16 complexity | 82eec261940cc73c4a0e912340604bc9 MD5 | raw file
  1. require 'active_support/core_ext/object/blank'
  2. require 'active_support/core_ext/string/filters'
  3. require 'action_view/helpers/tag_helper'
  4. module ActionView
  5. # = Action View Text Helpers
  6. module Helpers #:nodoc:
  7. # The TextHelper module provides a set of methods for filtering, formatting
  8. # and transforming strings, which can reduce the amount of inline Ruby code in
  9. # your views. These helper methods extend Action View making them callable
  10. # within your template files.
  11. #
  12. # ==== Sanitization
  13. #
  14. # Most text helpers by default sanitize the given content, but do not escape it.
  15. # This means HTML tags will appear in the page but all malicious code will be removed.
  16. # Let's look at some examples using the +simple_format+ method:
  17. #
  18. # simple_format('<a href="http://example.com/">Example</a>')
  19. # # => "<p><a href=\"http://example.com/\">Example</a></p>"
  20. #
  21. # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
  22. # # => "<p><a>Example</a></p>"
  23. #
  24. # If you want to escape all content, you should invoke the +h+ method before
  25. # calling the text helper.
  26. #
  27. # simple_format h('<a href="http://example.com/">Example</a>')
  28. # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
  29. module TextHelper
  30. extend ActiveSupport::Concern
  31. include SanitizeHelper
  32. # The preferred method of outputting text in your views is to use the
  33. # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
  34. # do not operate as expected in an eRuby code block. If you absolutely must
  35. # output text within a non-output code block (i.e., <% %>), you can use the concat method.
  36. #
  37. # ==== Examples
  38. # <%
  39. # concat "hello"
  40. # # is the equivalent of <%= "hello" %>
  41. #
  42. # if logged_in
  43. # concat "Logged in!"
  44. # else
  45. # concat link_to('login', :action => login)
  46. # end
  47. # # will either display "Logged in!" or a login link
  48. # %>
  49. def concat(string)
  50. output_buffer << string
  51. end
  52. def safe_concat(string)
  53. output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
  54. end
  55. # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
  56. # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
  57. # for a total length not exceeding <tt>:length</tt>.
  58. #
  59. # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
  60. #
  61. # The result is not marked as HTML-safe, so will be subject to the default escaping when
  62. # used in views, unless wrapped by <tt>raw()</tt>. Care should be taken if +text+ contains HTML tags
  63. # or entities, because truncation may produce invalid HTML (such as unbalanced or incomplete tags).
  64. #
  65. # ==== Examples
  66. #
  67. # truncate("Once upon a time in a world far far away")
  68. # # => "Once upon a time in a world..."
  69. #
  70. # truncate("Once upon a time in a world far far away", :length => 17)
  71. # # => "Once upon a ti..."
  72. #
  73. # truncate("Once upon a time in a world far far away", :length => 17, :separator => ' ')
  74. # # => "Once upon a..."
  75. #
  76. # truncate("And they found that many people were sleeping better.", :length => 25, :omission => '... (continued)')
  77. # # => "And they f... (continued)"
  78. #
  79. # truncate("<p>Once upon a time in a world far far away</p>")
  80. # # => "<p>Once upon a time in a wo..."
  81. def truncate(text, options = {})
  82. options.reverse_merge!(:length => 30)
  83. text.truncate(options.delete(:length), options) if text
  84. end
  85. # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
  86. # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
  87. # as a single-quoted string with \1 where the phrase is to be inserted (defaults to
  88. # '<strong class="highlight">\1</strong>')
  89. #
  90. # ==== Examples
  91. # highlight('You searched for: rails', 'rails')
  92. # # => You searched for: <strong class="highlight">rails</strong>
  93. #
  94. # highlight('You searched for: ruby, rails, dhh', 'actionpack')
  95. # # => You searched for: ruby, rails, dhh
  96. #
  97. # highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
  98. # # => You searched <em>for</em>: <em>rails</em>
  99. #
  100. # highlight('You searched for: rails', 'rails', :highlighter => '<a href="search?q=\1">\1</a>')
  101. # # => You searched for: <a href="search?q=rails">rails</a>
  102. #
  103. # You can still use <tt>highlight</tt> with the old API that accepts the
  104. # +highlighter+ as its optional third parameter:
  105. # highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>') # => You searched for: <a href="search?q=rails">rails</a>
  106. def highlight(text, phrases, *args)
  107. options = args.extract_options!
  108. unless args.empty?
  109. options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>'
  110. end
  111. options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>')
  112. text = sanitize(text) unless options[:sanitize] == false
  113. if text.blank? || phrases.blank?
  114. text
  115. else
  116. match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
  117. text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter])
  118. end.html_safe
  119. end
  120. # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
  121. # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
  122. # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
  123. # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The resulting string
  124. # will be stripped in any case. If the +phrase+ isn't found, nil is returned.
  125. #
  126. # ==== Examples
  127. # excerpt('This is an example', 'an', :radius => 5)
  128. # # => ...s is an exam...
  129. #
  130. # excerpt('This is an example', 'is', :radius => 5)
  131. # # => This is a...
  132. #
  133. # excerpt('This is an example', 'is')
  134. # # => This is an example
  135. #
  136. # excerpt('This next thing is an example', 'ex', :radius => 2)
  137. # # => ...next...
  138. #
  139. # excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ')
  140. # # => <chop> is also an example
  141. #
  142. # You can still use <tt>excerpt</tt> with the old API that accepts the
  143. # +radius+ as its optional third and the +ellipsis+ as its
  144. # optional forth parameter:
  145. # excerpt('This is an example', 'an', 5) # => ...s is an exam...
  146. # excerpt('This is also an example', 'an', 8, '<chop> ') # => <chop> is also an example
  147. def excerpt(text, phrase, *args)
  148. return unless text && phrase
  149. options = args.extract_options!
  150. unless args.empty?
  151. options[:radius] = args[0] || 100
  152. options[:omission] = args[1] || "..."
  153. end
  154. options.reverse_merge!(:radius => 100, :omission => "...")
  155. phrase = Regexp.escape(phrase)
  156. return unless found_pos = text.mb_chars =~ /(#{phrase})/i
  157. start_pos = [ found_pos - options[:radius], 0 ].max
  158. end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min
  159. prefix = start_pos > 0 ? options[:omission] : ""
  160. postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : ""
  161. prefix + text.mb_chars[start_pos..end_pos].strip + postfix
  162. end
  163. # Attempts to pluralize the +singular+ word unless +count+ is 1. If
  164. # +plural+ is supplied, it will use that when count is > 1, otherwise
  165. # it will use the Inflector to determine the plural form
  166. #
  167. # ==== Examples
  168. # pluralize(1, 'person')
  169. # # => 1 person
  170. #
  171. # pluralize(2, 'person')
  172. # # => 2 people
  173. #
  174. # pluralize(3, 'person', 'users')
  175. # # => 3 users
  176. #
  177. # pluralize(0, 'person')
  178. # # => 0 people
  179. def pluralize(count, singular, plural = nil)
  180. "#{count || 0} " + ((count == 1 || count =~ /^1(\.0+)?$/) ? singular : (plural || singular.pluralize))
  181. end
  182. # Wraps the +text+ into lines no longer than +line_width+ width. This method
  183. # breaks on the first whitespace character that does not exceed +line_width+
  184. # (which is 80 by default).
  185. #
  186. # ==== Examples
  187. #
  188. # word_wrap('Once upon a time')
  189. # # => Once upon a time
  190. #
  191. # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
  192. # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\n a successor to the throne turned out to be more trouble than anyone could have\n imagined...
  193. #
  194. # word_wrap('Once upon a time', :line_width => 8)
  195. # # => Once upon\na time
  196. #
  197. # word_wrap('Once upon a time', :line_width => 1)
  198. # # => Once\nupon\na\ntime
  199. #
  200. # You can still use <tt>word_wrap</tt> with the old API that accepts the
  201. # +line_width+ as its optional second parameter:
  202. # word_wrap('Once upon a time', 8) # => Once upon\na time
  203. def word_wrap(text, *args)
  204. options = args.extract_options!
  205. unless args.blank?
  206. options[:line_width] = args[0] || 80
  207. end
  208. options.reverse_merge!(:line_width => 80)
  209. text.split("\n").collect do |line|
  210. line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
  211. end * "\n"
  212. end
  213. # Returns +text+ transformed into HTML using simple formatting rules.
  214. # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
  215. # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
  216. # considered as a linebreak and a <tt><br /></tt> tag is appended. This
  217. # method does not remove the newlines from the +text+.
  218. #
  219. # You can pass any HTML attributes into <tt>html_options</tt>. These
  220. # will be added to all created paragraphs.
  221. #
  222. # ==== Options
  223. # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
  224. #
  225. # ==== Examples
  226. # my_text = "Here is some basic text...\n...with a line break."
  227. #
  228. # simple_format(my_text)
  229. # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
  230. #
  231. # more_text = "We want to put a paragraph...\n\n...right there."
  232. #
  233. # simple_format(more_text)
  234. # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
  235. #
  236. # simple_format("Look ma! A class!", :class => 'description')
  237. # # => "<p class='description'>Look ma! A class!</p>"
  238. #
  239. # simple_format("<span>I'm allowed!</span> It's true.", {}, :sanitize => false)
  240. # # => "<p><span>I'm allowed!</span> It's true.</p>"
  241. def simple_format(text, html_options={}, options={})
  242. text = '' if text.nil?
  243. text = text.dup
  244. start_tag = tag('p', html_options, true)
  245. text = sanitize(text) unless options[:sanitize] == false
  246. text = text.to_str
  247. text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
  248. text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}") # 2+ newline -> paragraph
  249. text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
  250. text.insert 0, start_tag
  251. text.html_safe.safe_concat("</p>")
  252. end
  253. # Creates a Cycle object whose _to_s_ method cycles through elements of an
  254. # array every time it is called. This can be used for example, to alternate
  255. # classes for table rows. You can use named cycles to allow nesting in loops.
  256. # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
  257. # named cycle. The default name for a cycle without a +:name+ key is
  258. # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
  259. # and passing the name of the cycle. The current cycle string can be obtained
  260. # anytime using the current_cycle method.
  261. #
  262. # ==== Examples
  263. # # Alternate CSS classes for even and odd numbers...
  264. # @items = [1,2,3,4]
  265. # <table>
  266. # <% @items.each do |item| %>
  267. # <tr class="<%= cycle("odd", "even") -%>">
  268. # <td>item</td>
  269. # </tr>
  270. # <% end %>
  271. # </table>
  272. #
  273. #
  274. # # Cycle CSS classes for rows, and text colors for values within each row
  275. # @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'},
  276. # {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'},
  277. # {:first => 'June', :middle => 'Dae', :last => 'Jones'}]
  278. # <% @items.each do |item| %>
  279. # <tr class="<%= cycle("odd", "even", :name => "row_class") -%>">
  280. # <td>
  281. # <% item.values.each do |value| %>
  282. # <%# Create a named cycle "colors" %>
  283. # <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
  284. # <%= value %>
  285. # </span>
  286. # <% end %>
  287. # <% reset_cycle("colors") %>
  288. # </td>
  289. # </tr>
  290. # <% end %>
  291. def cycle(first_value, *values)
  292. if (values.last.instance_of? Hash)
  293. params = values.pop
  294. name = params[:name]
  295. else
  296. name = "default"
  297. end
  298. values.unshift(first_value)
  299. cycle = get_cycle(name)
  300. unless cycle && cycle.values == values
  301. cycle = set_cycle(name, Cycle.new(*values))
  302. end
  303. cycle.to_s
  304. end
  305. # Returns the current cycle string after a cycle has been started. Useful
  306. # for complex table highlighting or any other design need which requires
  307. # the current cycle string in more than one place.
  308. #
  309. # ==== Example
  310. # # Alternate background colors
  311. # @items = [1,2,3,4]
  312. # <% @items.each do |item| %>
  313. # <div style="background-color:<%= cycle("red","white","blue") %>">
  314. # <span style="background-color:<%= current_cycle %>"><%= item %></span>
  315. # </div>
  316. # <% end %>
  317. def current_cycle(name = "default")
  318. cycle = get_cycle(name)
  319. cycle.current_value if cycle
  320. end
  321. # Resets a cycle so that it starts from the first element the next time
  322. # it is called. Pass in +name+ to reset a named cycle.
  323. #
  324. # ==== Example
  325. # # Alternate CSS classes for even and odd numbers...
  326. # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
  327. # <table>
  328. # <% @items.each do |item| %>
  329. # <tr class="<%= cycle("even", "odd") -%>">
  330. # <% item.each do |value| %>
  331. # <span style="color:<%= cycle("#333", "#666", "#999", :name => "colors") -%>">
  332. # <%= value %>
  333. # </span>
  334. # <% end %>
  335. #
  336. # <% reset_cycle("colors") %>
  337. # </tr>
  338. # <% end %>
  339. # </table>
  340. def reset_cycle(name = "default")
  341. cycle = get_cycle(name)
  342. cycle.reset if cycle
  343. end
  344. class Cycle #:nodoc:
  345. attr_reader :values
  346. def initialize(first_value, *values)
  347. @values = values.unshift(first_value)
  348. reset
  349. end
  350. def reset
  351. @index = 0
  352. end
  353. def current_value
  354. @values[previous_index].to_s
  355. end
  356. def to_s
  357. value = @values[@index].to_s
  358. @index = next_index
  359. return value
  360. end
  361. private
  362. def next_index
  363. step_index(1)
  364. end
  365. def previous_index
  366. step_index(-1)
  367. end
  368. def step_index(n)
  369. (@index + n) % @values.size
  370. end
  371. end
  372. private
  373. # The cycle helpers need to store the cycles in a place that is
  374. # guaranteed to be reset every time a page is rendered, so it
  375. # uses an instance variable of ActionView::Base.
  376. def get_cycle(name)
  377. @_cycles = Hash.new unless defined?(@_cycles)
  378. return @_cycles[name]
  379. end
  380. def set_cycle(name, cycle_object)
  381. @_cycles = Hash.new unless defined?(@_cycles)
  382. @_cycles[name] = cycle_object
  383. end
  384. end
  385. end
  386. end