sitepress/sitepress

View on GitHub
sitepress-rails/rails/app/controllers/concerns/sitepress/site_pages.rb

Summary

Maintainability
A
25 mins
Test Coverage
require "cgi"

module Sitepress
  # Serves up Sitepress site pages in a rails application. This is mixed into the
  # Sitepress::SiteController, but may be included into other controllers for static
  # page behavior.
  module SitePages
    # Rails 5 requires a format to be given to the private layout method
    # to return the path to the layout.
    DEFAULT_PAGE_RAILS_FORMATS = [:html].freeze

    # Default root path of resources.
    ROOT_RESOURCE_PATH = "".freeze

    extend ActiveSupport::Concern

    included do
      rescue_from Sitepress::ResourceNotFound, with: :resource_not_found
      helper Sitepress::Engine.helpers
      helper_method :current_page, :site, :page_rendition
      around_action :ensure_site_reload
    end

    # Public method that is primarily called by Rails to display the page. This should
    # be hooked up to the Rails routes file.
    def show
      render_resource requested_resource
    end

    protected

    # If a resource has a handler, (e.g. erb, haml, etc.) we say its "renderable" and
    # process it. If it doesn't have a handler, we treat it like its just a plain ol'
    # file and serve it up.
    def render_resource(resource)
      if resource.renderable?
        # Set this as our "top-level" resource. We might change it again in the pre-render
        # method to deal with rendering resources inside of resources.
        @current_resource = resource
        render_resource_with_handler resource
      else
        send_binary_resource resource
      end
    end

    # Renders the markup within a resource that can be rendered.
    def page_rendition(resource, layout: nil)
      Rendition.new(resource).tap do |rendition|
        rendition.layout = layout
        pre_render rendition
      end
    end

    # If a resource has a handler (e.g. erb, haml, etc.) we use the Rails renderer to
    # process templates, layouts, partials, etc. To keep the whole rendering process
    # contained in a way that the end user can override, we coupled the resource, source
    # and output within a `Rendition` object so that it may be processed via hooks.
    def render_resource_with_handler(resource)
      # Add the resource path to the view path so that partials can be rendered
      append_relative_partial_path resource

      rendition = page_rendition(resource, layout: controller_layout(resource))

      # Fire a callback in the controller in case anybody needs it.
      process_rendition rendition

      # Now we finally render the output of the processed rendition to the client.
      post_render rendition
    end

    # This is where the actual rendering happens for the page source in Rails.
    def pre_render(rendition)
      original_resource = @current_resource
      begin
        # This sets the `current_page` and `current_resource` variable equal to the given resource.
        @current_resource = rendition.resource
        rendition.output = render_to_string inline: rendition.source,
          type: rendition.handler,
          layout: rendition.layout
      ensure
        @current_resource = original_resource
      end
    end

    # This is to be used by end users if they need to do any post-processing on the rendering page.
    # For example, the user may use Nokogiri to parse static HTML pages and hook it into the asset pipeline.
    # They may also use tools like `HTMLPipeline` to process links from a markdown renderer.
    def process_rendition(rendition)
      # Do nothing unless the user extends this method.
    end

    # Send the inline rendered, post-processed string into the Rails rendering method that actually sends
    # the output to the end-user as a web response.
    def post_render(rendition)
      render body: rendition.output, content_type: rendition.mime_type
    end

    # A reference to the current resource that's being requested.
    attr_reader :current_resource

    # In templates resources are more naturally thought of as pages, so we call it `current_page` from
    # there and from the controller.
    alias :current_page :current_resource

    # References the singleton Site from the Sitepress::Configuration object. If you try to make this a class
    # variable and let Rails have multiple Sitepress sites, you might run into issues with respect to the asset
    # pipeline and various path configurations. To make this possible, a new object should be introduced to
    # Sitepress that manages a many-sites to one-rails instance so there's no path issues.
    def site
      Sitepress.site
    end

    # Raises a routing error for Rails to deal with in a more "standard" way if the user doesn't
    # override this method.
    def resource_not_found(e)
      raise ActionController::RoutingError, e.message
    end

    private
    # This makes it possible to render partials from the current resource with relative
    # paths. Without this the paths would have to be absolute.
    def append_relative_partial_path(resource)
      append_view_path resource.asset.path.dirname
    end

    def send_binary_resource(resource)
      send_file resource.asset.path,
        disposition: :inline,
        type: resource.mime_type.to_s
    end

    # Sitepress::ResourceNotFound is handled in the default Sitepress::SiteController
    # with an exception that Rails can use to display a 404 error.
    def get(path)
      resource = site.resources.get(path)
      if resource.nil?
        raise Sitepress::ResourceNotFound, "No such page: #{path}"
      else
        Rails.logger.info "Sitepress resolved asset #{resource.asset.path}"
        resource
      end
    end

    # Default finder of the resource for the current controller context. If the :resource_path
    # isn't present, then its probably the root path so grab that.
    def requested_resource
      get resource_request_path
    end

    # Returns the path of the resource in a way thats properly escape.
    def resource_request_path
      CGI.unescape request.path
    end

    # Returns the current layout for the inline Sitepress renderer. This is
    # exposed via some really convoluted private methods inside of the various
    # versions of Rails, so I try my best to hack out the path to the layout below.
    def controller_layout(resource)
      private_layout_method = self.method(:_layout)
      layout =
        if Rails.version >= "6"
          private_layout_method.call lookup_context, resource_rails_formats(resource)
        elsif Rails.version >= "5"
          private_layout_method.call resource_rails_formats(resource)
        else
          private_layout_method.call
        end

      if layout.instance_of? String # Rails 4 and 5 return a string from above.
        layout
      elsif layout # Rails 3 and older return an object that gives us a file name
        File.basename(layout.identifier).split('.').first
      else
        # If none of the conditions are met, then no layout was
        # specified, so nil is returned.
        nil
      end
    end

    # Rails 5 requires an extension, like `:html`, to resolve a template. This
    # method returns the intersection of the formats Rails supports from Mime::Types
    # and the current page's node formats. If nothing intersects, HTML is returned
    # as a default.
    def resource_rails_formats(resource)
      node_formats = resource.node.formats
      supported_formats = node_formats & Mime::EXTENSION_LOOKUP.keys

      if supported_formats.empty?
        DEFAULT_PAGE_RAILS_FORMATS
      else
        supported_formats.map?(&:to_sym)
      end
    end

    # When in development mode, the site is reloaded and rebuilt between each request so
    # that users can see changes to content and site structure. These rebuilds are unnecessary and
    # slow per-request in a production environment, so they should not be reloaded.
    def ensure_site_reload
      yield
    ensure
      reload_site
    end

    # Drops the website cache so that it's rebuilt when called again.
    def reload_site
      site.reload! if reload_site?
    end

    # Looks at the configuration to see if the site should be reloaded between requests.
    def reload_site?
      !Sitepress.configuration.cache_resources
    end
  end
end