text_helper.rb 25.6 KB
Newer Older
1
require 'action_view/helpers/tag_helper'
2 3 4 5 6 7 8 9 10 11

begin
  require 'html/document'
rescue LoadError
  html_scanner_path = "#{File.dirname(__FILE__)}/../../action_controller/vendor/html-scanner"
  if File.directory?(html_scanner_path)
    $:.unshift html_scanner_path
    require 'html/document'
  end
end
12

D
Initial  
David Heinemeier Hansson 已提交
13 14
module ActionView
  module Helpers #:nodoc:
15 16 17
    # The TextHelper module provides a set of methods for filtering, formatting
    # and transforming strings, which can reduce the amount of inline Ruby code in
    # your views. These helper methods extend ActionView making them callable
18
    # within your template files.
19 20 21 22
    module TextHelper
      # The preferred method of outputting text in your views is to use the
      # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
      # do not operate as expected in an eRuby code block. If you absolutely must
23
      # output text within a non-output code block (i.e., <% %>), you can use the concat method.
D
David Heinemeier Hansson 已提交
24
      #
25
      # ==== Examples
26
      #   <%
27
      #       concat "hello"
28 29 30
      #       # is the equivalent of <%= "hello" %>
      #
      #       if (logged_in == true):
31
      #         concat "Logged in!"
32
      #       else
33
      #         concat link_to('login', :action => login)
34 35 36
      #       end
      #       # will either display "Logged in!" or a login link
      #   %>
37 38
      def concat(string, unused_binding = nil)
        if unused_binding
39
          ActiveSupport::Deprecation.warn("The binding argument of #concat is no longer needed.  Please remove it from your views and helpers.", caller)
40 41
        end

42
        output_buffer << string
D
Initial  
David Heinemeier Hansson 已提交
43 44
      end

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
      # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
      # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...").
      #
      # ==== Examples
      #
      #   truncate("Once upon a time in a world far far away")
      #   # => Once upon a time in a world f...
      #
      #   truncate("Once upon a time in a world far far away", :length => 14)
      #   # => Once upon a...
      #
      #   truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)")
      #   # => And they found that many (clipped)
      #
      #   truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15)
      #   # => And they found... (continued)
      #
      # You can still use <tt>truncate</tt> with the old API that accepts the
      # +length+ as its optional second and the +ellipsis+ as its
      # optional third parameter:
      #   truncate("Once upon a time in a world far far away", 14)
      #   # => Once upon a time in a world f...
      #
      #   truncate("And they found that many people were sleeping better.", 15, "... (continued)")
      #   # => And they found... (continued)
      def truncate(text, *args)
        options = args.extract_options!
        unless args.empty?
          ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' +
            'length and omission arguments', caller)
75

76 77
          options[:length] = args[0] || 30
          options[:omission] = args[1] || "..."
78
        end
79
        options.reverse_merge!(:length => 30, :omission => "...")
80

81 82 83 84
        if text
          l = options[:length] - options[:omission].mb_chars.length
          chars = text.mb_chars
          (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s
85
        end
D
Initial  
David Heinemeier Hansson 已提交
86 87
      end

88
      # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
89
      # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
90 91
      # as a single-quoted string with \1 where the phrase is to be inserted (defaults to
      # '<strong class="highlight">\1</strong>')
D
David Heinemeier Hansson 已提交
92
      #
93
      # ==== Examples
94
      #   highlight('You searched for: rails', 'rails')
95 96
      #   # => You searched for: <strong class="highlight">rails</strong>
      #
97
      #   highlight('You searched for: ruby, rails, dhh', 'actionpack')
98
      #   # => You searched for: ruby, rails, dhh
99
      #
100
      #   highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
101
      #   # => You searched <em>for</em>: <em>rails</em>
102
      #
103 104 105 106 107 108 109 110 111 112 113 114 115
      #   highlight('You searched for: rails', 'rails', :highlighter => '<a href="search?q=\1">\1</a>')
      #   # => You searched for: <a href="search?q=rails">rails</a>
      #
      # You can still use <tt>highlight</tt> with the old API that accepts the
      # +highlighter+ as its optional third parameter:
      #   highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>')     # => You searched for: <a href="search?q=rails">rails</a>
      def highlight(text, phrases, *args)
        options = args.extract_options!
        unless args.empty?
          options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>'
        end
        options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>')

116 117 118 119
        if text.blank? || phrases.blank?
          text
        else
          match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
120
          text.gsub(/(#{match})/i, options[:highlighter])
121
        end
D
Initial  
David Heinemeier Hansson 已提交
122
      end
123

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
      # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
      # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
      # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
      # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The resulting string
      # will be stripped in any case. If the +phrase+ isn't found, nil is returned.
      #
      # ==== Examples
      #   excerpt('This is an example', 'an', :radius => 5)
      #   # => ...s is an exam...
      #
      #   excerpt('This is an example', 'is', :radius => 5)
      #   # => This is a...
      #
      #   excerpt('This is an example', 'is')
      #   # => This is an example
      #
      #   excerpt('This next thing is an example', 'ex', :radius => 2)
      #   # => ...next...
      #
      #   excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ')
      #   # => <chop> is also an example
      #
      # You can still use <tt>excerpt</tt> with the old API that accepts the
      # +radius+ as its optional third and the +ellipsis+ as its
      # optional forth parameter:
      #   excerpt('This is an example', 'an', 5)                   # => ...s is an exam...
      #   excerpt('This is also an example', 'an', 8, '<chop> ')   # => <chop> is also an example
      def excerpt(text, phrase, *args)
        options = args.extract_options!
        unless args.empty?
          options[:radius] = args[0] || 100
          options[:omission] = args[1] || "..."
        end
        options.reverse_merge!(:radius => 100, :omission => "...")
158

159 160
        if text && phrase
          phrase = Regexp.escape(phrase)
161

162 163 164
          if found_pos = text.mb_chars =~ /(#{phrase})/i
            start_pos = [ found_pos - options[:radius], 0 ].max
            end_pos   = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min
D
Initial  
David Heinemeier Hansson 已提交
165

166 167
            prefix  = start_pos > 0 ? options[:omission] : ""
            postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : ""
D
Initial  
David Heinemeier Hansson 已提交
168

169 170 171
            prefix + text.mb_chars[start_pos..end_pos].strip + postfix
          else
            nil
172 173
          end
        end
174
      end
D
Initial  
David Heinemeier Hansson 已提交
175

176 177 178
      # Attempts to pluralize the +singular+ word unless +count+ is 1. If
      # +plural+ is supplied, it will use that when count is > 1, otherwise
      # it will use the Inflector to determine the plural form
D
David Heinemeier Hansson 已提交
179
      #
180
      # ==== Examples
181
      #   pluralize(1, 'person')
182 183
      #   # => 1 person
      #
184
      #   pluralize(2, 'person')
185 186
      #   # => 2 people
      #
187
      #   pluralize(3, 'person', 'users')
188 189 190 191
      #   # => 3 users
      #
      #   pluralize(0, 'person')
      #   # => 0 people
D
Initial  
David Heinemeier Hansson 已提交
192
      def pluralize(count, singular, plural = nil)
193
        "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
D
Initial  
David Heinemeier Hansson 已提交
194 195
      end

D
David Heinemeier Hansson 已提交
196
      # Wraps the +text+ into lines no longer than +line_width+ width. This method
197 198
      # breaks on the first whitespace character that does not exceed +line_width+
      # (which is 80 by default).
D
David Heinemeier Hansson 已提交
199
      #
200 201 202 203 204
      # ==== Examples
      #
      #   word_wrap('Once upon a time')
      #   # => Once upon a time
      #
205 206 207 208 209 210 211
      #   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...')
      #   # => 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...
      #
      #   word_wrap('Once upon a time', :line_width => 8)
      #   # => Once upon\na time
      #
      #   word_wrap('Once upon a time', :line_width => 1)
212
      #   # => Once\nupon\na\ntime
213 214 215 216 217 218 219 220 221 222 223
      #
      # You can still use <tt>word_wrap</tt> with the old API that accepts the
      # +line_width+ as its optional second parameter:
      #   word_wrap('Once upon a time', 8)     # => Once upon\na time
      def word_wrap(text, *args)
        options = args.extract_options!
        unless args.blank?
          options[:line_width] = args[0] || 80
        end
        options.reverse_merge!(:line_width => 80)

224
        text.split("\n").collect do |line|
225
          line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
226
        end * "\n"
227 228
      end

D
Initial  
David Heinemeier Hansson 已提交
229
      begin
230
        require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth)
D
Initial  
David Heinemeier Hansson 已提交
231

232 233 234
        # Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags.
        #
        # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
D
David Heinemeier Hansson 已提交
235 236
        # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
        # is available</i>.
237 238 239 240 241 242 243 244 245 246 247 248 249
        #
        # ==== Examples
        #   textilize("*This is Textile!*  Rejoice!")
        #   # => "<p><strong>This is Textile!</strong>  Rejoice!</p>"
        #
        #   textilize("I _love_ ROR(Ruby on Rails)!")
        #   # => "<p>I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!</p>"
        #
        #   textilize("h2. Textile makes markup -easy- simple!")
        #   # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
        #
        #   textilize("Visit the Rails website "here":http://www.rubyonrails.org/.)
        #   # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>"
D
Initial  
David Heinemeier Hansson 已提交
250
        def textilize(text)
251 252 253 254
          if text.blank?
            ""
          else
            textilized = RedCloth.new(text, [ :hard_breaks ])
255
            textilized.hard_breaks = true if textilized.respond_to?(:hard_breaks=)
256
            textilized.to_html
257
          end
D
Initial  
David Heinemeier Hansson 已提交
258 259
        end

260
        # Returns the text with all the Textile codes turned into HTML tags,
D
David Heinemeier Hansson 已提交
261
        # but without the bounding <p> tag that RedCloth adds.
262 263
        #
        # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
D
David Heinemeier Hansson 已提交
264 265
        # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
        # is available</i>.
266 267 268 269 270 271 272 273 274 275 276 277 278
        #
        # ==== Examples
        #   textilize_without_paragraph("*This is Textile!*  Rejoice!")
        #   # => "<strong>This is Textile!</strong>  Rejoice!"
        #
        #   textilize_without_paragraph("I _love_ ROR(Ruby on Rails)!")
        #   # => "I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!"
        #
        #   textilize_without_paragraph("h2. Textile makes markup -easy- simple!")
        #   # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
        #
        #   textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.)
        #   # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>."
D
Initial  
David Heinemeier Hansson 已提交
279 280 281 282 283 284 285 286 287 288 289
        def textilize_without_paragraph(text)
          textiled = textilize(text)
          if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
          if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
          return textiled
        end
      rescue LoadError
        # We can't really help what's not there
      end

      begin
290
        require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth)
D
Initial  
David Heinemeier Hansson 已提交
291

D
David Heinemeier Hansson 已提交
292 293 294
        # Returns the text with all the Markdown codes turned into HTML tags.
        # <i>This method is only available if BlueCloth[http://www.deveiate.org/projects/BlueCloth]
        # is available</i>.
295 296 297 298 299 300 301 302 303
        #
        # ==== Examples
        #   markdown("We are using __Markdown__ now!")
        #   # => "<p>We are using <strong>Markdown</strong> now!</p>"
        #
        #   markdown("We like to _write_ `code`, not just _read_ it!")
        #   # => "<p>We like to <em>write</em> <code>code</code>, not just <em>read</em> it!</p>"
        #
        #   markdown("The [Markdown website](http://daringfireball.net/projects/markdown/) has more information.")
304
        #   # => "<p>The <a href="http://daringfireball.net/projects/markdown/">Markdown website</a>
305 306 307
        #   #     has more information.</p>"
        #
        #   markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")')
308
        #   # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>'
D
Initial  
David Heinemeier Hansson 已提交
309
        def markdown(text)
310
          text.blank? ? "" : BlueCloth.new(text).to_html
D
Initial  
David Heinemeier Hansson 已提交
311 312 313 314
        end
      rescue LoadError
        # We can't really help what's not there
      end
315

D
David Heinemeier Hansson 已提交
316
      # Returns +text+ transformed into HTML using simple formatting rules.
317
      # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
D
David Heinemeier Hansson 已提交
318 319
      # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
      # considered as a linebreak and a <tt><br /></tt> tag is appended. This
320
      # method does not remove the newlines from the +text+.
321
      #
322
      # You can pass any HTML attributes into <tt>html_options</tt>.  These
323
      # will be added to all created paragraphs.
324
      # ==== Examples
325
      #   my_text = "Here is some basic text...\n...with a line break."
326 327
      #
      #   simple_format(my_text)
328
      #   # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
329
      #
330
      #   more_text = "We want to put a paragraph...\n\n...right there."
331 332
      #
      #   simple_format(more_text)
333
      #   # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
334 335 336 337 338 339 340 341 342 343 344
      #
      #   simple_format("Look ma! A class!", :class => 'description')
      #   # => "<p class='description'>Look ma! A class!</p>"
      def simple_format(text, html_options={})
        start_tag = tag('p', html_options, true)
        text = text.to_s.dup
        text.gsub!(/\r\n?/, "\n")                    # \r\n and \r -> \n
        text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}")  # 2+ newline  -> paragraph
        text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline   -> br
        text.insert 0, start_tag
        text << "</p>"
345
      end
D
Initial  
David Heinemeier Hansson 已提交
346

347
      # Turns all URLs and e-mail addresses into clickable links. The +link+ parameter
348
      # will limit what should be linked. You can add HTML attributes to the links using
349 350
      # +href_options+. Options for +link+ are <tt>:all</tt> (default),
      # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and
351 352 353
      # e-mail address is yielded and the result is used as the link text.
      #
      # ==== Examples
354
      #   auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com")
D
David Heinemeier Hansson 已提交
355 356
      #   # => "Go to <a href=\"http://www.rubyonrails.org\">http://www.rubyonrails.org</a> and
      #   #     say hello to <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
357
      #
358
      #   auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :urls)
359
      #   # => "Visit <a href=\"http://www.loudthinking.com/\">http://www.loudthinking.com/</a>
360
      #   #     or e-mail david@loudthinking.com"
361
      #
362 363
      #   auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :email_addresses)
      #   # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
D
David Heinemeier Hansson 已提交
364
      #
365 366
      #   post_body = "Welcome to my new blog at http://www.myblog.com/.  Please e-mail me at me@email.com."
      #   auto_link(post_body, :all, :target => '_blank') do |text|
367 368
      #     truncate(text, 15)
      #   end
369
      #   # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
370
      #         Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
371
      #
372 373 374 375 376 377 378 379 380 381 382 383 384
      #
      # You can still use <tt>auto_link</tt> with the old API that accepts the
      # +link+ as its optional second parameter and the +html_options+ hash
      # as its optional third parameter:
      #   post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
      #   auto_link(post_body, :urls)     # => Once upon\na time
      #   # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\">http://www.myblog.com</a>.
      #         Please e-mail me at me@email.com."
      #
      #   auto_link(post_body, :all, :target => "_blank")     # => Once upon\na time
      #   # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>.
      #         Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
      def auto_link(text, *args, &block)#link = :all, href_options = {}, &block)
385
        return '' if text.blank?
386 387 388 389 390 391 392 393 394 395 396 397

        options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter
        unless args.empty?
          options[:link] = args[0] || :all
          options[:html] = args[1] || {}
        end
        options.reverse_merge!(:link => :all, :html => {})

        case options[:link].to_sym
          when :all                         then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), &block)
          when :email_addresses             then auto_link_email_addresses(text, &block)
          when :urls                        then auto_link_urls(text, options[:html], &block)
398
        end
399
      end
400

D
David Heinemeier Hansson 已提交
401
      # Creates a Cycle object whose _to_s_ method cycles through elements of an
402 403 404
      # array every time it is called. This can be used for example, to alternate
      # classes for table rows.  You can use named cycles to allow nesting in loops.
      # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
405 406 407 408
      # named cycle. The default name for a cycle without a +:name+ key is
      # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
      # and passing the name of the cycle. The current cycle string can be obtained
      # anytime using the current_cycle method.
409
      #
410
      # ==== Examples
411 412 413
      #   # Alternate CSS classes for even and odd numbers...
      #   @items = [1,2,3,4]
      #   <table>
D
David Heinemeier Hansson 已提交
414 415 416
      #   <% @items.each do |item| %>
      #     <tr class="<%= cycle("even", "odd") -%>">
      #       <td>item</td>
417
      #     </tr>
D
David Heinemeier Hansson 已提交
418
      #   <% end %>
419
      #   </table>
420 421
      #
      #
422
      #   # Cycle CSS classes for rows, and text colors for values within each row
423 424
      #   @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'},
      #                {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'},
425
      #               {:first => 'June', :middle => 'Dae', :last => 'Jones'}]
D
David Heinemeier Hansson 已提交
426
      #   <% @items.each do |item| %>
427
      #     <tr class="<%= cycle("even", "odd", :name => "row_class") -%>">
428
      #       <td>
D
David Heinemeier Hansson 已提交
429
      #         <% item.values.each do |value| %>
D
David Heinemeier Hansson 已提交
430
      #           <%# Create a named cycle "colors" %>
D
David Heinemeier Hansson 已提交
431
      #           <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
432
      #             <%= value %>
433
      #           </span>
D
David Heinemeier Hansson 已提交
434 435
      #         <% end %>
      #         <% reset_cycle("colors") %>
436 437
      #       </td>
      #    </tr>
D
David Heinemeier Hansson 已提交
438
      #  <% end %>
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
      def cycle(first_value, *values)
        if (values.last.instance_of? Hash)
          params = values.pop
          name = params[:name]
        else
          name = "default"
        end
        values.unshift(first_value)

        cycle = get_cycle(name)
        if (cycle.nil? || cycle.values != values)
          cycle = set_cycle(name, Cycle.new(*values))
        end
        return cycle.to_s
      end
454

455 456 457 458 459 460 461 462 463
      # Returns the current cycle string after a cycle has been started. Useful
      # for complex table highlighing or any other design need which requires
      # the current cycle string in more than one place.
      #
      # ==== Example
      #   # Alternate background colors
      #   @items = [1,2,3,4]
      #   <% @items.each do |item| %>
      #     <div style="background-color:<%= cycle("red","white","blue") %>">
P
Pratik Naik 已提交
464
      #       <span style="background-color:<%= current_cycle %>"><%= item %></span>
465 466 467 468 469 470 471
      #     </div>
      #   <% end %>
      def current_cycle(name = "default")
        cycle = get_cycle(name)
        cycle.current_value unless cycle.nil?
      end

472
      # Resets a cycle so that it starts from the first element the next time
D
David Heinemeier Hansson 已提交
473
      # it is called. Pass in +name+ to reset a named cycle.
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
      #
      # ==== Example
      #   # Alternate CSS classes for even and odd numbers...
      #   @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
      #   <table>
      #   <% @items.each do |item| %>
      #     <tr class="<%= cycle("even", "odd") -%>">
      #         <% item.each do |value| %>
      #           <span style="color:<%= cycle("#333", "#666", "#999", :name => "colors") -%>">
      #             <%= value %>
      #           </span>
      #         <% end %>
      #
      #         <% reset_cycle("colors") %>
      #     </tr>
      #   <% end %>
      #   </table>
491 492
      def reset_cycle(name = "default")
        cycle = get_cycle(name)
493
        cycle.reset unless cycle.nil?
494 495
      end

496
      class Cycle #:nodoc:
497
        attr_reader :values
498

499 500 501 502
        def initialize(first_value, *values)
          @values = values.unshift(first_value)
          reset
        end
503

504 505 506 507
        def reset
          @index = 0
        end

508 509 510 511
        def current_value
          @values[previous_index].to_s
        end

512 513
        def to_s
          value = @values[@index].to_s
514
          @index = next_index
515 516
          return value
        end
517 518 519 520 521 522 523 524 525 526 527 528 529 530

        private

        def next_index
          step_index(1)
        end

        def previous_index
          step_index(-1)
        end

        def step_index(n)
          (@index + n) % @values.size
        end
531
      end
532

D
Initial  
David Heinemeier Hansson 已提交
533
      private
534 535 536 537
        # The cycle helpers need to store the cycles in a place that is
        # guaranteed to be reset every time a page is rendered, so it
        # uses an instance variable of ActionView::Base.
        def get_cycle(name)
538
          @_cycles = Hash.new unless defined?(@_cycles)
539 540
          return @_cycles[name]
        end
541

542
        def set_cycle(name, cycle_object)
543
          @_cycles = Hash.new unless defined?(@_cycles)
544 545
          @_cycles[name] = cycle_object
        end
546

547
        AUTO_LINK_RE = %r{
548 549
                        (                          # leading text
                          <\w+.*?>|                # leading HTML tag, or
550
                          [^=!:'"/]|               # leading punctuation, or
551
                          ^                        # beginning of line
552 553
                        )
                        (
554 555
                          (?:https?://)|           # protocol spec, or
                          (?:www\.)                # www.*
556
                        )
557
                        (
558 559 560
                          [-\w]+                   # subdomain or domain
                          (?:\.[-\w]+)*            # remaining subdomains or domain
                          (?::\d+)?                # port
561
                          (?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path
562
                          (?:\?[\w\+@%&=.;-]+)?     # query string
563
                          (?:\#[\w\-]*)?           # trailing anchor
564
                        )
565
                        ([[:punct:]]|<|$|)       # trailing text
566
                       }x unless const_defined?(:AUTO_LINK_RE)
567

568
        # Turns all urls into clickable links.  If a block is given, each url
D
David Heinemeier Hansson 已提交
569
        # is yielded and the result is used as the link text.
570 571
        def auto_link_urls(text, html_options = {})
          extra_options = tag_options(html_options.stringify_keys) || ""
572
          text.gsub(AUTO_LINK_RE) do
573
            all, a, b, c, d = $&, $1, $2, $3, $4
574 575 576
            if a =~ /<a\s/i # don't replace URL's that are already linked
              all
            else
577 578 579
              text = b + c
              text = yield(text) if block_given?
              %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{text}</a>#{d})
580 581
            end
          end
582 583
        end

584 585
        # Turns all email addresses into clickable links.  If a block is given,
        # each email is yielded and the result is used as the link text.
586
        def auto_link_email_addresses(text)
587
          body = text.dup
588 589
          text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
            text = $1
590

591 592 593 594 595 596
            if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/)
              text
            else
              display_text = (block_given?) ? yield(text) : text
              %{<a href="mailto:#{text}">#{display_text}</a>}
            end
597
          end
598
        end
D
Initial  
David Heinemeier Hansson 已提交
599 600
    end
  end
601
end