lib/roda/plugins/view_options.rb



# frozen-string-literal: true

require_relative 'render'

#
class Roda
  module RodaPlugins
    # The view_options plugin allows you to override view and layout
    # options for specific branches and routes.
    #
    #   plugin :render
    #   plugin :view_options
    #
    #   route do |r|
    #     r.on "users" do
    #       set_layout_options template: 'users_layout'
    #       set_view_options engine: 'haml'
    #
    #       # ...
    #     end
    #   end
    #
    # The options you specify via the set_view_options and
    # set_layout_options methods 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 Integer do |id|
    #         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: {escape: false}
    #
    #     r.on "users" do
    #       # Escape <%= %> in this branch
    #       set_view_options template_opts: {escape: true}
    #     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

      module InstanceMethods
        # 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

        # Set branch/route options to use when rendering the layout
        def set_layout_options(opts)
          if options = @_layout_options
            @_layout_options = options.merge!(opts)
          else
            @_layout_options = opts
          end
        end

        # Set branch/route options to use when rendering the view
        def set_view_options(opts)
          if options = @_view_options
            @_view_options = options.merge!(opts)
          else
            @_view_options = opts
          end
        end

        private

        if Render::COMPILED_METHOD_SUPPORT
          # Return nil if using custom view or layout options.
          # If using a view subdir, prefix the template key with the subdir.
          def _cached_template_method_key(template)
            return if @_view_options || @_layout_options

            if subdir = @_view_subdir
              template = [subdir, template]
            end

            super
          end

          # Return nil if using custom view or layout options.
          # If using a view subdir, prefix the template key with the subdir.
          def _cached_template_method_lookup(method_cache, template)
            return if @_view_options || @_layout_options

            if subdir = @_view_subdir
              template = [subdir, template]
            end

            super
          end
        end

        # 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

          if !t_opts[:_is_layout] && (v_opts = @_view_options)
            t_opts.merge!(v_opts)
          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

          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