require 'rack/mime' require 'middleman-core/sitemap/extensions/traversal' require 'middleman-core/file_renderer' require 'middleman-core/template_renderer' require 'middleman-core/contracts' module Middleman # Sitemap namespace module Sitemap # Sitemap Resource class class Resource include Contracts include Middleman::Sitemap::Extensions::Traversal # The source path of this resource (relative to the source directory, # without template extensions) # @return [String] attr_reader :path # The output path in the build directory for this resource # @return [String] attr_accessor :destination_path # The on-disk source file for this resource, if there is one # @return [String] Contract Maybe[IsA['Middleman::SourceFile']] attr_reader :file_descriptor # The path to use when requesting this resource. Normally it's # the same as {#destination_path} but it can be overridden in subclasses. # @return [String] alias_method :request_path, :destination_path METADATA_HASH = ({ options: Maybe[Hash], locals: Maybe[Hash], page: Maybe[Hash] }) # The metadata for this resource # @return [Hash] Contract METADATA_HASH attr_reader :metadata # Initialize resource with parent store and URL # @param [Middleman::Sitemap::Store] store # @param [String] path # @param [String] source Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Or[IsA['Middleman::SourceFile'], String]] => Any def initialize(store, path, source=nil) @store = store @app = @store.app @path = path source = Pathname(source) if source && source.is_a?(String) if source && source.is_a?(Pathname) @file_descriptor = ::Middleman::SourceFile.new(source.relative_path_from(@app.source_dir), source, @app.source_dir, Set.new([:source])) else @file_descriptor = source end @destination_path = @path # Options are generally rendering/sitemap options # Locals are local variables for rendering this resource's template # Page are data that is exposed through this resource's data member. # Note: It is named 'page' for backwards compatibility with older MM. @metadata = { options: {}, locals: {}, page: {} } end # Whether this resource has a template file # @return [Boolean] Contract Bool def template? return false if file_descriptor.nil? !::Tilt[file_descriptor[:full_path].to_s].nil? end # Backwards compatible method for turning descriptor into a string. # @return [String] Contract String def source_file file_descriptor && file_descriptor[:full_path].to_s end # Merge in new metadata specific to this resource. # @param [Hash] meta A metadata block with keys :options, :locals, :page. # Options are generally rendering/sitemap options # Locals are local variables for rendering this resource's template # Page are data that is exposed through this resource's data member. # Note: It is named 'page' for backwards compatibility with older MM. Contract METADATA_HASH => METADATA_HASH def add_metadata(meta={}) @metadata.deep_merge!(meta) end # Data about this resource, populated from frontmatter or extensions. # @return [Hash] Contract RespondTo[:indifferent_access?] def data ::Middleman::Util.recursively_enhance(metadata[:page]) end # Options about how this resource is rendered, such as its :layout, # :renderer_options, and whether or not to use :directory_indexes. # @return [Hash] Contract Hash def options metadata[:options] end # Local variable mappings that are used when rendering the template for this resource. # @return [Hash] Contract Hash def locals metadata[:locals] end # Extension of the path (i.e. '.js') # @return [String] Contract String def ext File.extname(path) end # Render this resource # @return [String] Contract Hash, Hash => String def render(opts={}, locs={}) return ::Middleman::FileRenderer.new(@app, file_descriptor[:full_path].to_s).template_data_for_file unless template? ::Middleman::Util.instrument 'render.resource', path: file_descriptor[:full_path].to_s, destination_path: destination_path do md = metadata opts = md[:options].deep_merge(opts) locs = md[:locals].deep_merge(locs) locs[:current_path] ||= destination_path # Certain output file types don't use layouts unless opts.key?(:layout) opts[:layout] = false if %w(.js .json .css .txt).include?(ext) end renderer = ::Middleman::TemplateRenderer.new(@app, file_descriptor[:full_path].to_s) renderer.render(locs, opts) end end # A path without the directory index - so foo/index.html becomes # just foo. Best for linking. # @return [String] Contract String def url url_path = destination_path if @app.config[:strip_index_file] url_path = url_path.sub(/(^|\/)#{Regexp.escape(@app.config[:index_file])}$/, @app.config[:trailing_slash] ? '/' : '') end File.join(@app.config[:http_prefix], url_path) end # Whether the source file is binary. # # @return [Boolean] Contract Bool def binary? !file_descriptor.nil? && ::Middleman::Util.binary?(file_descriptor[:full_path].to_s) end # Ignore a resource directly, without going through the whole # ignore filter stuff. # @return [void] Contract Any def ignore! @ignored = true end # Whether the Resource is ignored # @return [Boolean] Contract Bool def ignored? return true if @ignored # Ignore based on the source path (without template extensions) return true if @app.sitemap.ignored?(path) # This allows files to be ignored by their source file name (with template extensions) if !self.is_a?(ProxyResource) && file_descriptor && @app.sitemap.ignored?(file_descriptor[:relative_path].to_s) true else false end end # The preferred MIME content type for this resource based on extension or metadata # @return [String] MIME type for this resource Contract Maybe[String] def content_type options[:content_type] || ::Rack::Mime.mime_type(ext, nil) end def to_s "#" end alias_method :inspect, :to_s # Ruby 2.0 calls inspect for NoMethodError instead of to_s end class StringResource < Resource def initialize(store, path, contents=nil, &block) @request_path = path @contents = block_given? ? block : contents super(store, path) end def template? true end def render(*) @contents.respond_to?(:call) ? @contents.call : @contents end def binary? false end end end end