lib/roda/plugins/view_options.rb



class Roda
  module RodaPlugins
    # The view_options plugin allows you to override view and layout
    # options and locals for specific branches and routes.
    #
    #   plugin :render
    #   plugin :view_options
    #
    #   route do |r|
    #     r.on "users" do
    #       set_layout_options :template=>'users_layout'
    #       set_layout_locals :title=>'Users'
    #       set_view_options :engine=>'haml'
    #       set_view_locals :footer=>'(c) Roda'
    #
    #       # ...
    #     end
    #   end
    #
    # The options and locals you specify have higher precedence than
    # the render plugin options, but lower precedence than options
    # you directly pass to the view/render methods.
    #
    # = View Subdirectories
    #
    # The view_options plugin also has special support for sites
    # that have outgrown a flat view directory and use subdirectories
    # for views.  It allows you to set the view directory to
    # use, and template names that do not contain a slash will
    # automatically use that view subdirectory.  Example:
    #
    #   plugin :render, :layout=>'./layout'
    #   plugin :view_options
    #
    #   route do |r|
    #     r.on "users" do
    #       set_view_subdir 'users'
    #       
    #       r.get :id do
    #         append_view_subdir 'profile'
    #         view 'index' # uses ./views/users/profile/index.erb
    #       end
    #
    #       r.get 'list' do
    #         view 'lists/users' # uses ./views/lists/users.erb
    #       end
    #     end
    #   end
    #
    # Note that when a view subdirectory is set, the layout will
    # also be looked up in the subdirectory unless it contains
    # a slash.  So if you want to use a view subdirectory for
    # templates but have a shared layout, you should make sure your
    # layout contains a slash, similar to the example above.
    #
    # = Per-branch HTML escaping
    #
    # If you have an existing Roda application that doesn't use
    # automatic HTML escaping for <tt><%= %></tt> tags via the
    # :render plugin's :escape option, but you want to switch to
    # using the :escape option, you can now do so without making
    # all changes at once.  With set_view_options, you can now
    # specify escaping or not on a per branch basis in the routing
    # tree:
    #
    #   plugin :render, :escape=>true
    #   plugin :view_options
    #
    #   route do |r|
    #     # Don't escape <%= %> by default
    #     set_view_options :template_opts=>{:engine_class=>nil}
    #
    #     r.on "users" do
    #       # Escape <%= %> in this branch
    #       set_view_options :template_opts=>{:engine_class=>render_opts[:template_opts][:engine_class]}
    #     end
    #   end
    module ViewOptions
      # Load the render plugin before this plugin, since this plugin
      # works by overriding methods in the render plugin.
      def self.load_dependencies(app)
        app.plugin :render
      end

      # The following methods are created via metaprogramming:
      # set_layout_locals :: Set locals to use in the layout
      # set_layout_options :: Set options to use when rendering the layout
      # set_view_locals :: Set locals to use in the view
      # set_view_options :: Set options to use when rendering the view
      module InstanceMethods
        %w'layout view'.each do |type|
          %w'locals options'.each do |var|
            v = "_#{type}_#{var}"
            module_eval(<<-END, __FILE__, __LINE__+1)
              def set#{v}(opts)
                if @#{v}
                  @#{v} = Hash[@#{v}].merge!(opts)
                else
                  @#{v} = opts
                end
              end
            END
          end
        end

        # Append a view subdirectory to use.  If there hasn't already
        # been a view subdirectory set, this just sets it to the argument.
        # If there has already been a view subdirectory set, this sets
        # the view subdirectory to a subdirectory of the existing
        # view subdirectory.
        def append_view_subdir(v)
          if subdir = @_view_subdir
            set_view_subdir("#{subdir}/#{v}")
          else
            set_view_subdir(v)
          end
        end

        # Set the view subdirectory to use.  This can be set to nil
        # to not use a view subdirectory.
        def set_view_subdir(v)
          @_view_subdir = v
        end

        private

        # If view options or locals have been set and this
        # template isn't a layout template, merge the options
        # and locals into the returned hash.
        def parse_template_opts(template, opts)
          t_opts = super

          unless t_opts[:_is_layout]
            if v_opts = @_view_options
              t_opts.merge!(v_opts)
            end

            if v_locals = @_view_locals
              t_opts[:locals] = if t_locals = t_opts[:locals]
                Hash[v_locals].merge!(t_locals)
              else
                v_locals
              end
            end
          end

          t_opts
        end

        # If layout options or locals have been set,
        # merge the options and locals into the returned hash.
        def render_layout_opts
          opts = super

          if l_opts = @_layout_options
            opts.merge!(l_opts)
          end

          if l_locals = @_layout_locals
            opts[:locals] = if o_locals = opts[:locals]
              Hash[o_locals].merge!(l_locals)
            else
              l_locals
            end
          end

          opts
        end

        # Override the template name to use the view subdirectory if the
        # there is a view subdirectory and the template name does not
        # contain a slash.
        def template_name(opts)
          name = super
          if (v = @_view_subdir) && name !~ /\//
            "#{v}/#{name}"
          else
            name
          end
        end
      end
    end

    register_plugin(:view_options, ViewOptions)
  end
end