document sitemap, make ignore accept paths, regexps and blocks

This commit is contained in:
Thomas Reynolds 2012-01-07 16:57:50 -08:00
parent 56e0c1755c
commit 0ac9c0b662
5 changed files with 241 additions and 78 deletions

View file

@ -2,6 +2,8 @@
require "rack" require "rack"
require "rack/test" require "rack/test"
require 'find'
# CLI Module # CLI Module
module Middleman::Cli module Middleman::Cli
@ -87,13 +89,16 @@ module Middleman::Cli
# Set the root path to the Middleman::Base's root # Set the root path to the Middleman::Base's root
source_root(shared_instance.root) source_root(shared_instance.root)
# Ignore following method
desc "", "", :hide => true
# Render a template to a file. # Render a template to a file.
# #
# @param [String] source # @param [String] source
# @param [String] destination # @param [String] destination
# @param [Hash] config # @param [Hash] config
# @return [String] the actual destination file path that was created # @return [String] the actual destination file path that was created
desc "", "", :hide => true
def tilt_template(source, destination, config={}) def tilt_template(source, destination, config={})
build_dir = self.class.shared_instance.build_dir build_dir = self.class.shared_instance.build_dir
request_path = destination.sub(/^#{build_dir}/, "") request_path = destination.sub(/^#{build_dir}/, "")

View file

@ -1,9 +1,10 @@
require "active_support/core_ext/hash/deep_merge" # Core Sitemap Extensions
require 'find'
module Middleman::CoreExtensions::Sitemap module Middleman::CoreExtensions::Sitemap
# Setup Extension
class << self class << self
# Once registered
def registered(app) def registered(app)
# Setup callbacks which can exclude paths from the sitemap
app.set :ignored_sitemap_matchers, { app.set :ignored_sitemap_matchers, {
# dotfiles and folders in the root # dotfiles and folders in the root
:root_dotfiles => proc { |file, path| file.match(/^\./) }, :root_dotfiles => proc { |file, path| file.match(/^\./) },
@ -19,50 +20,74 @@ module Middleman::CoreExtensions::Sitemap
# Files without any output extension (layouts, partials) # Files without any output extension (layouts, partials)
:extensionless => proc { |file, path| !path.match(/\./) }, :extensionless => proc { |file, path| !path.match(/\./) },
} }
# Include instance methods
app.send :include, InstanceMethods app.send :include, InstanceMethods
end end
alias :included :registered alias :included :registered
end end
# Sitemap instance methods
module InstanceMethods module InstanceMethods
# Extend initialize to listen for change events
def initialize def initialize
super super
static_path = source_dir.sub(self.root, "").sub(/^\//, "") # Cleanup paths
static_path = source_dir.sub(root, "").sub(/^\//, "")
sitemap_regex = static_path.empty? ? // : (%r{^#{static_path + "/"}}) sitemap_regex = static_path.empty? ? // : (%r{^#{static_path + "/"}})
self.files.changed sitemap_regex do |file| # Register file change callback
self.sitemap.touch_file(file) files.changed sitemap_regex do |file|
sitemap.touch_file(file)
end end
self.files.deleted sitemap_regex do |file| # Register file delete callback
self.sitemap.remove_file(file) files.deleted sitemap_regex do |file|
sitemap.remove_file(file)
end end
end end
# Get the sitemap class instance
# @return [Middleman::Sitemap::Store]
def sitemap def sitemap
@sitemap ||= ::Middleman::Sitemap::Store.new(self) @_sitemap ||= ::Middleman::Sitemap::Store.new(self)
end end
# Get the page object for the current path
# @return [Middleman::Sitemap::Page]
def current_page def current_page
sitemap.page(current_path) sitemap.page(current_path)
end end
# Keep a path from building # Ignore a path, regex or callback
def ignore(path) # @param [String, Regexp]
sitemap.ignore(path) # @return [void]
def ignore(*args)
sitemap.ignore(*args)
end end
def reroute(url, target) # Proxy one path to another
sitemap.proxy(url, target) # @param [String] url
# @param [String] target
# @return [void]
def reroute(*args)
sitemap.proxy(*args)
end end
# Register a handler to provide metadata on a file path
# @param [Regexp] matcher
# @return [Array<Array<Proc, Regexp>>]
def provides_metadata(matcher=nil, &block) def provides_metadata(matcher=nil, &block)
@_provides_metadata ||= [] @_provides_metadata ||= []
@_provides_metadata << [block, matcher] if block_given? @_provides_metadata << [block, matcher] if block_given?
@_provides_metadata @_provides_metadata
end end
# Register a handler to provide metadata on a url path
# @param [Regexp] matcher
# @return [Array<Array<Proc, Regexp>>]
def provides_metadata_for_path(matcher=nil, &block) def provides_metadata_for_path(matcher=nil, &block)
@_provides_metadata_for_path ||= [] @_provides_metadata_for_path ||= []
@_provides_metadata_for_path << [block, matcher] if block_given? @_provides_metadata_for_path << [block, matcher] if block_given?

View file

@ -1,7 +1,23 @@
# Sitemap namespace
module Middleman::Sitemap module Middleman::Sitemap
class Page
attr_accessor :store, :path, :proxied_to, :status
# Sitemap Page class
class Page
# @return [Middleman::Sitemap::Store]
attr_accessor :store
# @return [String]
attr_accessor :path
# @return [Middleman::Sitemap::Page]
attr_accessor :proxied_to
# @return [Symbol]
attr_accessor :status
# Initialize page with parent store and URL
# @param [Middleman::Sitemap::Store] store
# @param [String] path
def initialize(store, path) def initialize(store, path)
@store = store @store = store
@path = path @path = path
@ -10,6 +26,8 @@ module Middleman::Sitemap
@proxied_to = nil @proxied_to = nil
end end
# Whether this page has a template file
# @return [Boolean]
def template? def template?
if proxy? if proxy?
store.page(proxied_to).template? store.page(proxied_to).template?
@ -19,10 +37,8 @@ module Middleman::Sitemap
end end
end end
def source_file=(src) # Internal path to be requested when rendering this page
@source_file = src # @return [String]
end
def request_path def request_path
if proxy? if proxy?
store.page(proxied_to).path store.page(proxied_to).path
@ -31,6 +47,15 @@ module Middleman::Sitemap
end end
end end
# Set the on-disk source file for this page
# @param [String] src
# @return [void]
def source_file=(src)
@source_file = src
end
# The on-disk source file
# @return [String]
def source_file def source_file
if proxy? if proxy?
store.page(proxied_to).source_file store.page(proxied_to).source_file
@ -39,99 +64,124 @@ module Middleman::Sitemap
end end
end end
# The template instance
# @return [Middleman::Sitemap::Template]
def template def template
@_template ||= ::Middleman::Sitemap::Template.new(self) @_template ||= ::Middleman::Sitemap::Template.new(self)
end end
# Extension of the path
# @return [String]
def ext def ext
File.extname(path) File.extname(path)
end end
# Mime type of the path
# @return [String]
def mime_type def mime_type
app.mime_type ext app.mime_type ext
end end
# Whether this page is a proxy
# @return [Boolean]
def proxy? def proxy?
@status == :proxy @status == :proxy
end end
# Set this page to proxy to a target path
# @param [String] target
# @return [void]
def proxy_to(target) def proxy_to(target)
@status = :proxy @status = :proxy
@proxied_to = target @proxied_to = target
end end
# Whether this page is a generic page
# @return [Boolean]
def generic? def generic?
@status == :generic @status == :generic
end end
# Set this page to be a generic page
# @return [void]
def make_generic def make_generic
@status = :generic @status = :generic
# TODO: Remove from ignore array?
end end
# Whether this page is ignored
# @return [Boolean]
def ignored? def ignored?
@status == :ignored store.ignored?(self.path)
end end
# Set this page to be ignored
# @return [void]
def ignore def ignore
@status = :ignored store.ignore(self.path)
end end
# If this is a template, refresh contents
# @return [void]
def touch def touch
template.touch if template? template.touch if template?
end end
def custom_renderer(&block) # If this is a template, remove contents
@_custom_renderer ||= nil # @return [void]
@_custom_renderer = block if block_given? def delete
@_custom_renderer template.delete if template?
end end
# Render this page
# @return [String]
def render(*args, &block) def render(*args, &block)
return unless template? return unless template?
if proxy? if proxy?
# Forward blocks
# forward_blocks = template.blocks.compact
# forward_blocks << block if block_given?
t = store.page(proxied_to).template t = store.page(proxied_to).template
t.request_path = path t.request_path = path
t.render(*args) t.render(*args)
# do
# forward_blocks.each do |block|
# instance_exec(&block)
# end
# end
elsif !custom_renderer.nil?
params = args.dup
params << block if block_given?
instance_exec(*params, &custom_renderer)
else else
template.request_path = path template.request_path = path
template.render(*args, &block) template.render(*args, &block)
end end
end end
# Whether this page is a directory index
# @return [Boolean]
def directory_index? def directory_index?
path.include?(app.index_file) || path =~ /\/$/ || eponymous_directory? path.include?(app.index_file) || path =~ /\/$/ || eponymous_directory?
end end
# Whether this page is a eponymous_directory
# @return [Boolean]
def eponymous_directory? def eponymous_directory?
!!Dir.exists?(File.join(app.source_dir, eponymous_directory_path)) !!Dir.exists?(File.join(app.source_dir, eponymous_directory_path))
end end
# The path for the eponymous_directory of this page
# @return [String]
def eponymous_directory_path def eponymous_directory_path
path.sub('.html', '/').sub(/\/$/, "") + "/" path.sub('.html', '/').sub(/\/$/, "") + "/"
# TODO: Seems like .html shouldn't be hardcoded here
end end
# Get the relative path from the source
# @return [String]
def relative_path def relative_path
source_file.sub(app.source_dir, '') source_file.sub(app.source_dir, '')
end end
# This page's frontmatter
# @return [Hash, nil]
def data def data
data, content = app.frontmatter(relative_path) data, content = app.frontmatter(relative_path)
data || nil data || nil
end end
# This page's parent page
# @return [Middleman::Sitemap::Page, nil]
def parent def parent
parts = path.split("/") parts = path.split("/")
if path.include?(app.index_file) if path.include?(app.index_file)
@ -153,6 +203,8 @@ module Middleman::Sitemap
end end
end end
# This page's child pages
# @return [Array<Middleman::Sitemap::Page>]
def children def children
return [] unless directory_index? return [] unless directory_index?
@ -183,14 +235,19 @@ module Middleman::Sitemap
end.reject { |p| p.ignored? } end.reject { |p| p.ignored? }
end end
# This page's sibling pages
# @return [Array<Middleman::Sitemap::Page>]
def siblings def siblings
return [] unless parent return [] unless parent
parent.children.reject { |p| p == self } parent.children.reject { |p| p == self }
end end
protected protected
# This page's stored app
# @return [Middleman::Base]
def app def app
@store.app store.app
end end
end end
end end

View file

@ -1,34 +1,55 @@
# Sitemap namespace
module Middleman::Sitemap module Middleman::Sitemap
# The Store class
class Store class Store
# @return [Middleman::Base]
attr_accessor :app attr_accessor :app
# Initialize with parent app
# @param [Middleman::Base] app
def initialize(app) def initialize(app)
@app = app @app = app
@pages = {} @pages = {}
@ignored_paths = []
@ignored_regexes = []
@ignored_callbacks = []
end end
# Check to see if we know about a specific path # Check to see if we know about a specific path
# @param [String] path
# @return [Boolean]
def exists?(path) def exists?(path)
@pages.has_key?(path.sub(/^\//, "")) @pages.has_key?(path.sub(/^\//, ""))
end end
def set_context(path, opts={}, blk=nil) # Ignore a path or add an ignore callback
page(path) do # @param [String, Regexp] path
template.options = opts # @return [void]
template.blocks = [blk] def ignore(path=nil, &block)
if path.is_a? String
path_clean = path.sub(/^\//, "")
@ignored_paths << path_clean unless @ignored_paths.include?(path_clean)
elsif path.is_a? Regexp
@ignored_regexes << path unless @ignored_regexes.include?(path)
elsif block_given?
@ignored_callbacks << block
end end
end end
def ignore(path) # Setup a proxy from a path to a target
page(path) { ignore } # @param [String] path
app.cache.remove(:ignored_paths) # @param [String] target
end # @return [void]
def proxy(path, target) def proxy(path, target)
page(path) { proxy_to(target.sub(%r{^/}, "")) } page(path) { proxy_to(target.sub(%r{^/}, "")) }
app.cache.remove(:proxied_paths) app.cache.remove(:proxied_paths)
end end
# Get a page instance for a given path
# @param [String] path
# @return [Middleman::Sitemap::Page]
def page(path, &block) def page(path, &block)
path = path.sub(/^\//, "").gsub("%20", " ") path = path.sub(/^\//, "").gsub("%20", " ")
@pages[path] = ::Middleman::Sitemap::Page.new(self, path) unless @pages.has_key?(path) @pages[path] = ::Middleman::Sitemap::Page.new(self, path) unless @pages.has_key?(path)
@ -36,54 +57,86 @@ module Middleman::Sitemap
@pages[path] @pages[path]
end end
# Loop over known pages
# @return [void]
def each(&block) def each(&block)
@pages.each do |k, v| @pages.each do |k, v|
yield k, v yield k, v
end end
end end
# Get all known paths
# @return [Array<String>]
def all_paths def all_paths
@pages.keys @pages.keys
end end
# Whether a path is ignored
# @param [String] path
# @return [Boolean]
def ignored?(path) def ignored?(path)
ignored_paths.include?(path.sub(/^\//, "")) path_clean = path.sub(/^\//, "")
return true if @ignored_paths.include?(path_clean)
return true if @ignored_regexes.any? { |r| r.match(path_clean) }
return true if @ignored_callbacks.any? { |b| b.call(path_clean) }
false
end end
# Get a list of ignored paths
# @return [Array<String>]
def ignored_paths def ignored_paths
app.cache.fetch :ignored_paths do @pages.values.select(&:ignored?).map(&:path)
@pages.values.select(&:ignored?).map(&:path)
end
end end
# Whether the given path is generic
# @param [String] path
# @return [Boolean]
def generic?(path) def generic?(path)
generic_paths.include?(path.sub(/^\//, "")) generic_paths.include?(path.sub(/^\//, ""))
end end
# Get a list of generic paths
# @return [Array<String>]
def generic_paths def generic_paths
app.cache.fetch :generic_paths do app.cache.fetch :generic_paths do
@pages.values.select(&:generic?).map(&:path) @pages.values.select(&:generic?).map(&:path)
end end
end end
# Whether the given path is proxied
# @param [String] path
# @return [Boolean]
def proxied?(path) def proxied?(path)
proxied_paths.include?(path.sub(/^\//, "")) proxied_paths.include?(path.sub(/^\//, ""))
end end
# Get a list of proxied paths
# @return [Array<String>]
def proxied_paths def proxied_paths
app.cache.fetch :proxied_paths do app.cache.fetch :proxied_paths do
@pages.values.select(&:proxy?).map(&:path) @pages.values.select(&:proxy?).map(&:path)
end end
end end
# Remove a file from the store
# @param [String] file
# @return [void]
def remove_file(file) def remove_file(file)
path = file_to_path(file) path = file_to_path(file)
return false unless path return false unless path
path = path.sub(/^\//, "") path = path.sub(/^\//, "")
@pages.delete(path) if @pages.has_key?(path) if @pages.has_key?(path)
page(path).delete()
@pages.delete(path)
end
end end
# Get the URL path for an on-disk file
# @param [String] file
# @return [String]
def file_to_path(file) def file_to_path(file)
file = File.expand_path(file, @app.root) file = File.expand_path(file, @app.root)
@ -96,6 +149,9 @@ module Middleman::Sitemap
path path
end end
# Update or add an on-disk file path
# @param [String] file
# @return [Boolean]
def touch_file(file) def touch_file(file)
return false if file == @app.source_dir || File.directory?(file) return false if file == @app.source_dir || File.directory?(file)
@ -114,11 +170,19 @@ module Middleman::Sitemap
true true
end end
# Whether the sitemap should completely ignore a given file/path
# @param [String] file
# @param [String] path
# @return [Boolean]
def sitemap_should_ignore?(file, path) def sitemap_should_ignore?(file, path)
@app.sitemap_ignore.every(&:call) @app.sitemap_ignore.every(&:call)
end end
protected protected
# Get a path without templating extensions
# @param [String] file
# @param [String]
def extensionless_path(file) def extensionless_path(file)
app.cache.fetch(:extensionless_path, file) do app.cache.fetch(:extensionless_path, file) do
path = file.dup path = file.dup

View file

@ -1,8 +1,26 @@
# Used for merging results of metadata callbacks
require "active_support/core_ext/hash/deep_merge"
# Sitemap namespace
module Middleman::Sitemap module Middleman::Sitemap
# Template class
class Template class Template
attr_accessor :page, :options, :locals, :blocks, :request_path
# @return [Middleman::Sitemap::Page]
attr_accessor :page
# @return [Hash]
attr_accessor :options
# @return [Hash]
attr_accessor :locals
# @return [String]
attr_accessor :request_path
# Initialize template with parent page
# @param [Middleman::Sitemap:Page] page
def initialize(page) def initialize(page)
@page = page @page = page
@options = {} @options = {}
@ -10,30 +28,23 @@ module Middleman::Sitemap
@blocks = [] @blocks = []
end end
def path # Simple aliases
page.path delegate :path, :source_file, :store, :app, :ext, :to => :page
end
def source_file
page.source_file
end
def store
page.store
end
def app
store.app
end
def ext
page.ext
end
# Clear internal frontmatter cache for file if it changes
# @return [void]
def touch def touch
app.cache.remove(:metadata, source_file) app.cache.remove(:metadata, source_file)
end end
# Clear internal frontmatter cache for file if it is deleted
# @return [void]
def delete
app.cache.remove(:metadata, source_file)
end
# Get the metadata for both the current source_file and the current path
# @return [Hash]
def metadata def metadata
metadata = app.cache.fetch(:metadata, source_file) do metadata = app.cache.fetch(:metadata, source_file) do
data = { :options => {}, :locals => {}, :page => {}, :blocks => [] } data = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
@ -59,12 +70,17 @@ module Middleman::Sitemap
metadata[:blocks] << result[:blocks] metadata[:blocks] << result[:blocks]
result.delete(:blocks) result.delete(:blocks)
end end
metadata = metadata.deep_merge(result) metadata = metadata.deep_merge(result)
end end
metadata metadata
end end
# Render this template
# @param [Hash] opts
# @param [Hash] locs
# @return [String]
def render(opts={}, locs={}, &block) def render(opts={}, locs={}, &block)
puts "== Render Start: #{source_file}" if app.logging? puts "== Render Start: #{source_file}" if app.logging?
@ -77,10 +93,6 @@ module Middleman::Sitemap
app.data.store("page", md[:page]) app.data.store("page", md[:page])
end end
blocks.compact.each do |block|
app.instance_eval(&block)
end
md[:blocks].flatten.compact.each do |block| md[:blocks].flatten.compact.each do |block|
app.instance_eval(&block) app.instance_eval(&block)
end end