lib/action_view/helpers/capture_helper.rb



# frozen_string_literal: true

require "active_support/core_ext/string/output_safety"

module ActionView
  # = Action View Capture Helper
  module Helpers # :nodoc:
    # CaptureHelper exposes methods to let you extract generated markup which
    # can be used in other parts of a template or layout file.
    #
    # It provides a method to capture blocks into variables through capture and
    # a way to capture a block of markup for use in a layout through {content_for}[rdoc-ref:ActionView::Helpers::CaptureHelper#content_for].
    module CaptureHelper
      # The capture method extracts part of a template as a String object.
      # You can then use this object anywhere in your templates, layout, or helpers.
      #
      # The capture method can be used in ERB templates...
      #
      #   <% @greeting = capture do %>
      #     Welcome to my shiny new web page!  The date and time is
      #     <%= Time.now %>
      #   <% end %>
      #
      # ...and Builder (RXML) templates.
      #
      #   @timestamp = capture do
      #     "The current timestamp is #{Time.now}."
      #   end
      #
      # You can then use that variable anywhere else. For example:
      #
      #   <html>
      #   <head><title><%= @greeting %></title></head>
      #   <body>
      #   <b><%= @greeting %></b>
      #   </body>
      #   </html>
      #
      # The return of capture is the string generated by the block. For Example:
      #
      #   @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"
      #
      def capture(*args)
        value = nil
        buffer = with_output_buffer { value = yield(*args) }
        if (string = buffer.presence || value) && string.is_a?(String)
          ERB::Util.html_escape string
        end
      end

      # Calling <tt>content_for</tt> stores a block of markup in an identifier for later use.
      # In order to access this stored content in other templates, helper modules
      # or the layout, you would pass the identifier as an argument to <tt>content_for</tt>.
      #
      # Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling
      # <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does.
      #
      #   <% content_for :not_authorized do %>
      #     alert('You are not authorized to do that!')
      #   <% end %>
      #
      # You can then use <tt>content_for :not_authorized</tt> anywhere in your templates.
      #
      #   <%= content_for :not_authorized if current_user.nil? %>
      #
      # This is equivalent to:
      #
      #   <%= yield :not_authorized if current_user.nil? %>
      #
      # <tt>content_for</tt>, however, can also be used in helper modules.
      #
      #   module StorageHelper
      #     def stored_content
      #       content_for(:storage) || "Your storage is empty"
      #     end
      #   end
      #
      # This helper works just like normal helpers.
      #
      #   <%= stored_content %>
      #
      # You can also use the <tt>yield</tt> syntax alongside an existing call to
      # <tt>yield</tt> in a layout. For example:
      #
      #   <%# This is the layout %>
      #   <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
      #   <head>
      #     <title>My Website</title>
      #     <%= yield :script %>
      #   </head>
      #   <body>
      #     <%= yield %>
      #   </body>
      #   </html>
      #
      # And now, we'll create a view that has a <tt>content_for</tt> call that
      # creates the <tt>script</tt> identifier.
      #
      #   <%# This is our view %>
      #   Please login!
      #
      #   <% content_for :script do %>
      #     <script>alert('You are not authorized to view this page!')</script>
      #   <% end %>
      #
      # Then, in another view, you could to do something like this:
      #
      #   <%= link_to 'Logout', action: 'logout', remote: true %>
      #
      #   <% content_for :script do %>
      #     <%= javascript_include_tag :defaults %>
      #   <% end %>
      #
      # That will place +script+ tags for your default set of JavaScript files on the page;
      # this technique is useful if you'll only be using these scripts in a few views.
      #
      # Note that <tt>content_for</tt> concatenates (default) the blocks it is given for a particular
      # identifier in order. For example:
      #
      #   <% content_for :navigation do %>
      #     <li><%= link_to 'Home', action: 'index' %></li>
      #   <% end %>
      #
      # And in another place:
      #
      #   <% content_for :navigation do %>
      #     <li><%= link_to 'Login', action: 'login' %></li>
      #   <% end %>
      #
      # Then, in another template or layout, this code would render both links in order:
      #
      #   <ul><%= content_for :navigation %></ul>
      #
      # If the flush parameter is +true+ <tt>content_for</tt> replaces the blocks it is given. For example:
      #
      #   <% content_for :navigation do %>
      #     <li><%= link_to 'Home', action: 'index' %></li>
      #   <% end %>
      #
      #   <%# Add some other content, or use a different template: %>
      #
      #   <% content_for :navigation, flush: true do %>
      #     <li><%= link_to 'Login', action: 'login' %></li>
      #   <% end %>
      #
      # Then, in another template or layout, this code would render only the last link:
      #
      #   <ul><%= content_for :navigation %></ul>
      #
      # Lastly, simple content can be passed as a parameter:
      #
      #   <% content_for :script, javascript_include_tag(:defaults) %>
      #
      # WARNING: <tt>content_for</tt> is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
      def content_for(name, content = nil, options = {}, &block)
        if content || block_given?
          if block_given?
            options = content if content
            content = capture(&block)
          end
          if content
            options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
          end
          nil
        else
          @view_flow.get(name).presence
        end
      end

      # The same as +content_for+ but when used with streaming flushes
      # straight back to the layout. In other words, if you want to
      # concatenate several times to the same buffer when rendering a given
      # template, you should use +content_for+, if not, use +provide+ to tell
      # the layout to stop looking for more contents.
      def provide(name, content = nil, &block)
        content = capture(&block) if block_given?
        result = @view_flow.append!(name, content) if content
        result unless content
      end

      # <tt>content_for?</tt> checks whether any content has been captured yet using <tt>content_for</tt>.
      # Useful to render parts of your layout differently based on what is in your views.
      #
      #   <%# This is the layout %>
      #   <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
      #   <head>
      #     <title>My Website</title>
      #     <%= yield :script %>
      #   </head>
      #   <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>">
      #     <%= yield %>
      #     <%= yield :right_col %>
      #   </body>
      #   </html>
      def content_for?(name)
        @view_flow.get(name).present?
      end

      # Use an alternate output buffer for the duration of the block.
      # Defaults to a new empty string.
      def with_output_buffer(buf = nil) # :nodoc:
        unless buf
          buf = ActionView::OutputBuffer.new
          if output_buffer && output_buffer.respond_to?(:encoding)
            buf.force_encoding(output_buffer.encoding)
          end
        end
        self.output_buffer, old_buffer = buf, output_buffer
        yield
        output_buffer
      ensure
        self.output_buffer = old_buffer
      end
    end
  end
end