text_helper.rb 21.6 KB
Newer Older
1
require 'active_support/core_ext/object/blank'
2
require 'active_support/core_ext/string/filters'
3
require 'action_view/helpers/tag_helper'
4

D
Initial  
David Heinemeier Hansson 已提交
5
module ActionView
R
Rizwan Reza 已提交
6
  # = Action View Text Helpers
D
Initial  
David Heinemeier Hansson 已提交
7
  module Helpers #:nodoc:
8 9
    # The TextHelper module provides a set of methods for filtering, formatting
    # and transforming strings, which can reduce the amount of inline Ruby code in
10
    # your views. These helper methods extend Action View making them callable
11
    # within your template files.
12
    module TextHelper
13 14 15
      extend ActiveSupport::Concern

      include SanitizeHelper
16 17 18
      # 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
19
      # output text within a non-output code block (i.e., <% %>), you can use the concat method.
D
David Heinemeier Hansson 已提交
20
      #
21
      # ==== Examples
22
      #   <%
23
      #       concat "hello"
24 25
      #       # is the equivalent of <%= "hello" %>
      #
26
      #       if logged_in
27
      #         concat "Logged in!"
28
      #       else
29
      #         concat link_to('login', :action => login)
30 31 32
      #       end
      #       # will either display "Logged in!" or a login link
      #   %>
33
      def concat(string)
34
        output_buffer << string
D
Initial  
David Heinemeier Hansson 已提交
35 36
      end

37
      def safe_concat(string)
C
Carlhuda 已提交
38
        output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
39 40
      end

41
      # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
42 43 44
      # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
      # for a total length not exceeding <tt>:length</tt>.
      #
45
      # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
46 47 48 49
      #
      # The result is not marked as HTML-safe, so will be subject to the default escaping when
      # used in views, unless wrapped by <tt>raw()</tt>. Care should be taken if +text+ contains HTML tags
      # or entities, because truncation may produce invalid HTML (such as unbalanced or incomplete tags).
50 51 52 53
      #
      # ==== Examples
      #
      #   truncate("Once upon a time in a world far far away")
54
      #   # => "Once upon a time in a world..."
55
      #
56 57
      #   truncate("Once upon a time in a world far far away", :length => 17)
      #   # => "Once upon a ti..."
58
      #
59
      #   truncate("Once upon a time in a world far far away", :length => 17, :separator => ' ')
60
      #   # => "Once upon a..."
61
      #
62 63
      #   truncate("And they found that many people were sleeping better.", :length => 25, :omission => '... (continued)')
      #   # => "And they f... (continued)"
64
      #
65 66
      #   truncate("<p>Once upon a time in a world far far away</p>")
      #   # => "<p>Once upon a time in a wo..."
67
      def truncate(text, options = {})
68 69
        options.reverse_merge!(:length => 30)
        text.truncate(options.delete(:length), options) if text
D
Initial  
David Heinemeier Hansson 已提交
70 71
      end

72
      # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
73
      # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
74 75
      # 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 已提交
76
      #
77
      # ==== Examples
78
      #   highlight('You searched for: rails', 'rails')
79 80
      #   # => You searched for: <strong class="highlight">rails</strong>
      #
81
      #   highlight('You searched for: ruby, rails, dhh', 'actionpack')
82
      #   # => You searched for: ruby, rails, dhh
83
      #
84
      #   highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
85
      #   # => You searched <em>for</em>: <em>rails</em>
86
      #
87 88 89 90 91 92 93 94 95 96 97 98 99
      #   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>')

100
        text = sanitize(text) unless options[:sanitize] == false
101 102 103 104
        if text.blank? || phrases.blank?
          text
        else
          match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
105
          text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter])
106
        end.html_safe
D
Initial  
David Heinemeier Hansson 已提交
107
      end
108

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
      # 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 => "...")
143

144 145
        if text && phrase
          phrase = Regexp.escape(phrase)
146

147 148 149
          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 已提交
150

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

154 155 156
            prefix + text.mb_chars[start_pos..end_pos].strip + postfix
          else
            nil
157 158
          end
        end
159
      end
D
Initial  
David Heinemeier Hansson 已提交
160

161 162 163
      # 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 已提交
164
      #
165
      # ==== Examples
166
      #   pluralize(1, 'person')
167 168
      #   # => 1 person
      #
169
      #   pluralize(2, 'person')
170 171
      #   # => 2 people
      #
172
      #   pluralize(3, 'person', 'users')
173 174 175 176
      #   # => 3 users
      #
      #   pluralize(0, 'person')
      #   # => 0 people
D
Initial  
David Heinemeier Hansson 已提交
177
      def pluralize(count, singular, plural = nil)
178
        "#{count || 0} " + ((count == 1 || count =~ /^1(\.0+)?$/) ? singular : (plural || singular.pluralize))
D
Initial  
David Heinemeier Hansson 已提交
179 180
      end

D
David Heinemeier Hansson 已提交
181
      # Wraps the +text+ into lines no longer than +line_width+ width. This method
182 183
      # breaks on the first whitespace character that does not exceed +line_width+
      # (which is 80 by default).
D
David Heinemeier Hansson 已提交
184
      #
185 186 187 188 189
      # ==== Examples
      #
      #   word_wrap('Once upon a time')
      #   # => Once upon a time
      #
190 191 192 193 194 195 196
      #   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)
197
      #   # => Once\nupon\na\ntime
198 199 200 201 202 203 204 205 206 207 208
      #
      # 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)

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 213
      end

D
David Heinemeier Hansson 已提交
214
      # Returns +text+ transformed into HTML using simple formatting rules.
215
      # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
D
David Heinemeier Hansson 已提交
216 217
      # 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
218
      # method does not remove the newlines from the +text+.
219
      #
220
      # You can pass any HTML attributes into <tt>html_options</tt>.  These
221
      # will be added to all created paragraphs.
222
      # ==== Examples
223
      #   my_text = "Here is some basic text...\n...with a line break."
224 225
      #
      #   simple_format(my_text)
226
      #   # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
227
      #
228
      #   more_text = "We want to put a paragraph...\n\n...right there."
229 230
      #
      #   simple_format(more_text)
231
      #   # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
232 233 234
      #
      #   simple_format("Look ma! A class!", :class => 'description')
      #   # => "<p class='description'>Look ma! A class!</p>"
235
      def simple_format(text, html_options={}, options={})
236
        text = ''.html_safe if text.nil?
237
        start_tag = tag('p', html_options, true)
238
        text = sanitize(text) unless options[:sanitize] == false
239 240 241 242
        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
243
        text.html_safe.safe_concat("</p>")
244
      end
D
Initial  
David Heinemeier Hansson 已提交
245

P
Pratik Naik 已提交
246
      # Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option
247
      # will limit what should be linked. You can add HTML attributes to the links using
P
Pratik Naik 已提交
248
      # <tt>:html</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default),
249
      # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and
250 251 252
      # e-mail address is yielded and the result is used as the link text.
      #
      # ==== Examples
253
      #   auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com")
D
David Heinemeier Hansson 已提交
254 255
      #   # => "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>"
256
      #
P
Pratik Naik 已提交
257
      #   auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :link => :urls)
258
      #   # => "Visit <a href=\"http://www.loudthinking.com/\">http://www.loudthinking.com/</a>
259
      #   #     or e-mail david@loudthinking.com"
260
      #
P
Pratik Naik 已提交
261
      #   auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :link => :email_addresses)
262
      #   # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
D
David Heinemeier Hansson 已提交
263
      #
264
      #   post_body = "Welcome to my new blog at http://www.myblog.com/.  Please e-mail me at me@email.com."
P
Pratik Naik 已提交
265
      #   auto_link(post_body, :html => { :target => '_blank' }) do |text|
266 267
      #     truncate(text, 15)
      #   end
268
      #   # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
269
      #         Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
270
      #
271 272 273 274 275 276 277 278 279 280 281 282
      #
      # 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>."
P
Pratik Naik 已提交
283
      def auto_link(text, *args, &block)#link = :all, html = {}, &block)
284
        return ''.html_safe if text.blank?
285 286 287 288 289 290 291 292 293

        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
294
          when :all                         then auto_link_email_addresses(auto_link_urls(text, options[:html], options, &block), options[:html], &block)
295
          when :email_addresses             then auto_link_email_addresses(text, options[:html], &block)
296
          when :urls                        then auto_link_urls(text, options[:html], options, &block)
297
        end
298
      end
299

D
David Heinemeier Hansson 已提交
300
      # Creates a Cycle object whose _to_s_ method cycles through elements of an
301 302 303
      # 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
304 305 306 307
      # 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.
308
      #
309
      # ==== Examples
310 311 312
      #   # Alternate CSS classes for even and odd numbers...
      #   @items = [1,2,3,4]
      #   <table>
D
David Heinemeier Hansson 已提交
313 314 315
      #   <% @items.each do |item| %>
      #     <tr class="<%= cycle("even", "odd") -%>">
      #       <td>item</td>
316
      #     </tr>
D
David Heinemeier Hansson 已提交
317
      #   <% end %>
318
      #   </table>
319 320
      #
      #
321
      #   # Cycle CSS classes for rows, and text colors for values within each row
322 323
      #   @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'},
      #                {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'},
324
      #               {:first => 'June', :middle => 'Dae', :last => 'Jones'}]
D
David Heinemeier Hansson 已提交
325
      #   <% @items.each do |item| %>
326
      #     <tr class="<%= cycle("odd", "even", :name => "row_class") -%>">
327
      #       <td>
D
David Heinemeier Hansson 已提交
328
      #         <% item.values.each do |value| %>
D
David Heinemeier Hansson 已提交
329
      #           <%# Create a named cycle "colors" %>
D
David Heinemeier Hansson 已提交
330
      #           <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
331
      #             <%= value %>
332
      #           </span>
D
David Heinemeier Hansson 已提交
333 334
      #         <% end %>
      #         <% reset_cycle("colors") %>
335 336
      #       </td>
      #    </tr>
D
David Heinemeier Hansson 已提交
337
      #  <% end %>
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
      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
353

354
      # Returns the current cycle string after a cycle has been started. Useful
P
Pratik Naik 已提交
355
      # for complex table highlighting or any other design need which requires
356 357 358 359 360 361 362
      # 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 已提交
363
      #       <span style="background-color:<%= current_cycle %>"><%= item %></span>
364 365 366 367 368 369 370
      #     </div>
      #   <% end %>
      def current_cycle(name = "default")
        cycle = get_cycle(name)
        cycle.current_value unless cycle.nil?
      end

371
      # Resets a cycle so that it starts from the first element the next time
D
David Heinemeier Hansson 已提交
372
      # it is called. Pass in +name+ to reset a named cycle.
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
      #
      # ==== 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>
390 391
      def reset_cycle(name = "default")
        cycle = get_cycle(name)
392
        cycle.reset unless cycle.nil?
393 394
      end

395
      class Cycle #:nodoc:
396
        attr_reader :values
397

398 399 400 401
        def initialize(first_value, *values)
          @values = values.unshift(first_value)
          reset
        end
402

403 404 405 406
        def reset
          @index = 0
        end

407 408 409 410
        def current_value
          @values[previous_index].to_s
        end

411 412
        def to_s
          value = @values[@index].to_s
413
          @index = next_index
414 415
          return value
        end
416 417 418 419 420 421 422 423 424 425 426 427 428 429

        private

        def next_index
          step_index(1)
        end

        def previous_index
          step_index(-1)
        end

        def step_index(n)
          (@index + n) % @values.size
        end
430
      end
431

D
Initial  
David Heinemeier Hansson 已提交
432
      private
433 434 435 436
        # 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)
437
          @_cycles = Hash.new unless defined?(@_cycles)
438 439
          return @_cycles[name]
        end
440

441
        def set_cycle(name, cycle_object)
442
          @_cycles = Hash.new unless defined?(@_cycles)
443 444
          @_cycles[name] = cycle_object
        end
445

446
        AUTO_LINK_RE = %r{
447
            (?: ([\w+.:-]+:)// | www\. )
448
            [^\s<]+
449 450 451 452 453 454
          }x

        # regexps for determining context, used high-volume
        AUTO_LINK_CRE = [/<[^>]+$/, /^[^>]*>/, /<a\b.*?>/i, /<\/a>/i]

        AUTO_EMAIL_RE = /[\w.!#\$%+-]+@[\w-]+(?:\.[\w-]+)+/
455

456 457
        BRACKETS = { ']' => '[', ')' => '(', '}' => '{' }

458
        # Turns all urls into clickable links.  If a block is given, each url
D
David Heinemeier Hansson 已提交
459
        # is yielded and the result is used as the link text.
460
        def auto_link_urls(text, html_options = {}, options = {})
461
          link_attributes = html_options.stringify_keys
462
          text.gsub(AUTO_LINK_RE) do
463
            scheme, href = $1, $&
464
            punctuation = []
465 466
            
            if auto_linked?($`, $')
P
Pratik Naik 已提交
467
              # do not change string; URL is already linked
468 469 470
              href
            else
              # don't include trailing punctuation character as part of the URL
471 472 473 474 475
              while href.sub!(/[^\w\/-]$/, '')
                punctuation.push $&
                if opening = BRACKETS[punctuation.last] and href.scan(opening).size > href.scan(punctuation.last).size
                  href << punctuation.pop
                  break
476
                end
477 478 479
              end

              link_text = block_given?? yield(href) : href
480
              href = 'http://' + href unless scheme
481

482 483 484 485 486
              unless options[:sanitize] == false
                link_text = sanitize(link_text)
                href      = sanitize(href)
              end
              content_tag(:a, link_text, link_attributes.merge('href' => href), !!options[:sanitize]) + punctuation.reverse.join('')
487
            end
488
          end.html_safe
489 490
        end

491 492
        # 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.
493
        def auto_link_email_addresses(text, html_options = {}, options = {})
494 495
          text.gsub(AUTO_EMAIL_RE) do
            text = $&
496

497
            if auto_linked?($`, $')
498
              text.html_safe
499 500
            else
              display_text = (block_given?) ? yield(text) : text
501 502 503 504 505

              unless options[:sanitize] == false
                text         = sanitize(text)
                display_text = sanitize(display_text) unless text == display_text
              end
506
              mail_to text, display_text, html_options
507
            end
508
          end
509
        end
510 511 512 513 514 515
        
        # Detects already linked context or position in the middle of a tag
        def auto_linked?(left, right)
          (left =~ AUTO_LINK_CRE[0] and right =~ AUTO_LINK_CRE[1]) or
            (left.rindex(AUTO_LINK_CRE[2]) and $' !~ AUTO_LINK_CRE[3])
        end
D
Initial  
David Heinemeier Hansson 已提交
516 517
    end
  end
518
end