2009-04-23 18:58:38 -04:00
|
|
|
require "pathname"
|
2009-09-02 18:00:22 -04:00
|
|
|
require "active_support/core_ext/class"
|
2009-12-02 23:01:01 -05:00
|
|
|
require "action_view/template"
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2009-04-14 20:22:51 -04:00
|
|
|
module ActionView
|
2010-06-20 16:26:31 -04:00
|
|
|
# = Action View Resolver
|
2009-06-17 18:32:55 -04:00
|
|
|
class Resolver
|
2011-03-19 00:13:59 -04:00
|
|
|
|
|
|
|
class Path < String
|
|
|
|
attr_reader :name, :prefix, :partial, :virtual
|
|
|
|
alias_method :partial?, :partial
|
|
|
|
|
|
|
|
def initialize(name, prefix, partial)
|
|
|
|
@name, @prefix, @partial = name, prefix, partial
|
|
|
|
rebuild(@name, @prefix, @partial)
|
|
|
|
end
|
|
|
|
|
|
|
|
def rebuild(name, prefix, partial)
|
|
|
|
@virtual = ""
|
|
|
|
@virtual << "#{prefix}/" unless prefix.empty?
|
|
|
|
@virtual << (partial ? "_#{name}" : name)
|
|
|
|
|
|
|
|
self.replace(@virtual)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-12-16 15:37:48 -05:00
|
|
|
cattr_accessor :caching
|
|
|
|
self.caching = true
|
|
|
|
|
|
|
|
class << self
|
|
|
|
alias :caching? :caching
|
|
|
|
end
|
|
|
|
|
2010-03-07 06:49:27 -05:00
|
|
|
def initialize
|
2010-10-07 09:50:20 -04:00
|
|
|
@cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2|
|
|
|
|
h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } }
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
|
2010-03-08 17:13:24 -05:00
|
|
|
def clear_cache
|
|
|
|
@cached.clear
|
|
|
|
end
|
|
|
|
|
2010-03-08 10:32:40 -05:00
|
|
|
# Normalizes the arguments and passes it on to find_template.
|
2010-12-09 08:06:44 -05:00
|
|
|
def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
|
2010-10-10 03:24:17 -04:00
|
|
|
cached(key, [name, prefix, partial], details, locals) do
|
2010-03-08 17:13:24 -05:00
|
|
|
find_templates(name, prefix, partial, details)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-04-22 20:16:28 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
private
|
|
|
|
|
2010-12-16 15:37:48 -05:00
|
|
|
delegate :caching?, :to => "self.class"
|
2010-03-07 06:49:27 -05:00
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
# This is what child classes implement. No defaults are needed
|
|
|
|
# because Resolver guarantees that the arguments are present and
|
|
|
|
# normalized.
|
2010-03-08 17:13:24 -05:00
|
|
|
def find_templates(name, prefix, partial, details)
|
2011-02-09 08:32:40 -05:00
|
|
|
raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
# Helpers that builds a path. Useful for building virtual paths.
|
2010-10-10 03:24:17 -04:00
|
|
|
def build_path(name, prefix, partial)
|
2011-03-19 00:13:59 -04:00
|
|
|
Path.new(name, prefix, partial)
|
2010-10-07 07:26:58 -04:00
|
|
|
end
|
|
|
|
|
2010-12-27 02:44:51 -05:00
|
|
|
# Handles templates caching. If a key is given and caching is on
|
2010-10-10 03:24:17 -04:00
|
|
|
# always check the cache before hitting the resolver. Otherwise,
|
|
|
|
# it always hits the resolver but check if the resolver is fresher
|
|
|
|
# before returning it.
|
|
|
|
def cached(key, path_info, details, locals) #:nodoc:
|
|
|
|
name, prefix, partial = path_info
|
2010-10-07 09:50:20 -04:00
|
|
|
locals = sort_locals(locals)
|
2010-10-10 03:24:17 -04:00
|
|
|
|
|
|
|
if key && caching?
|
|
|
|
@cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals)
|
2010-10-07 09:50:20 -04:00
|
|
|
else
|
2010-10-10 03:24:17 -04:00
|
|
|
fresh = decorate(yield, path_info, details, locals)
|
|
|
|
return fresh unless key
|
|
|
|
|
|
|
|
scope = @cached[key][name][prefix][partial]
|
|
|
|
cache = scope[locals]
|
|
|
|
mtime = cache && cache.map(&:updated_at).max
|
|
|
|
|
|
|
|
if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime }
|
|
|
|
scope[locals] = fresh
|
|
|
|
else
|
|
|
|
cache
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Ensures all the resolver information is set in the template.
|
|
|
|
def decorate(templates, path_info, details, locals) #:nodoc:
|
|
|
|
cached = nil
|
|
|
|
templates.each do |t|
|
|
|
|
t.locals = locals
|
|
|
|
t.formats = details[:formats] || [:html] if t.formats.empty?
|
|
|
|
t.virtual_path ||= (cached ||= build_path(*path_info))
|
2010-10-07 09:50:20 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-10-10 03:24:17 -04:00
|
|
|
if :symbol.respond_to?("<=>")
|
|
|
|
def sort_locals(locals) #:nodoc:
|
2010-10-07 09:50:20 -04:00
|
|
|
locals.sort.freeze
|
|
|
|
end
|
|
|
|
else
|
2010-10-10 03:24:17 -04:00
|
|
|
def sort_locals(locals) #:nodoc:
|
2010-10-07 09:50:20 -04:00
|
|
|
locals = locals.map{ |l| l.to_s }
|
|
|
|
locals.sort!
|
|
|
|
locals.freeze
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
|
2009-09-02 18:00:22 -04:00
|
|
|
class PathResolver < Resolver
|
2011-03-19 00:13:59 -04:00
|
|
|
EXTENSIONS = [:locale, :formats, :handlers]
|
|
|
|
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}"
|
|
|
|
|
|
|
|
def initialize(pattern=nil)
|
|
|
|
@pattern = pattern || DEFAULT_PATTERN
|
|
|
|
super()
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
private
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-03-08 17:13:24 -05:00
|
|
|
def find_templates(name, prefix, partial, details)
|
2010-10-10 03:24:17 -04:00
|
|
|
path = build_path(name, prefix, partial)
|
2011-03-19 00:13:59 -04:00
|
|
|
extensions = Hash[EXTENSIONS.map { |ext| [ext, details[ext]] }.flatten(0)]
|
|
|
|
query(path, extensions, details[:formats])
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2010-03-19 12:20:15 -04:00
|
|
|
def query(path, exts, formats)
|
2011-03-19 00:13:59 -04:00
|
|
|
query = build_query(path, exts)
|
2010-11-28 16:26:16 -05:00
|
|
|
templates = []
|
|
|
|
sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] }
|
|
|
|
|
|
|
|
Dir[query].each do |p|
|
|
|
|
next if File.directory?(p) || !sanitizer[p].include?(p)
|
2010-05-17 11:41:54 -04:00
|
|
|
|
2010-11-28 16:26:16 -05:00
|
|
|
handler, format = extract_handler_and_format(p, formats)
|
2010-05-17 11:41:54 -04:00
|
|
|
contents = File.open(p, "rb") {|io| io.read }
|
|
|
|
|
2010-11-28 16:26:16 -05:00
|
|
|
templates << Template.new(contents, File.expand_path(p), handler,
|
2011-03-19 00:13:59 -04:00
|
|
|
:virtual_path => path.virtual, :format => format, :updated_at => mtime(p))
|
2009-12-28 19:28:26 -05:00
|
|
|
end
|
2010-11-28 16:26:16 -05:00
|
|
|
|
|
|
|
templates
|
2009-04-14 20:22:51 -04:00
|
|
|
end
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2011-03-19 00:13:59 -04:00
|
|
|
# Helper for building query glob string based on resolver's pattern.
|
|
|
|
def build_query(path, exts)
|
|
|
|
query = @pattern.dup
|
|
|
|
query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty...
|
|
|
|
query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name)
|
|
|
|
|
|
|
|
exts.each { |ext, variants|
|
|
|
|
query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
|
|
|
|
}
|
|
|
|
|
|
|
|
query.gsub!(/\.{html,/, ".{html,text.html,")
|
|
|
|
query.gsub!(/\.{text,/, ".{text,text.plain,")
|
|
|
|
|
|
|
|
File.expand_path(query, @path)
|
|
|
|
end
|
|
|
|
|
2010-10-10 03:24:17 -04:00
|
|
|
# Returns the file mtime from the filesystem.
|
|
|
|
def mtime(p)
|
|
|
|
File.stat(p).mtime
|
|
|
|
end
|
|
|
|
|
2010-03-19 12:20:15 -04:00
|
|
|
# Extract handler and formats from path. If a format cannot be a found neither
|
|
|
|
# from the path, or the handler, we should return the array of formats given
|
|
|
|
# to the resolver.
|
|
|
|
def extract_handler_and_format(path, default_formats)
|
2010-03-09 07:12:11 -05:00
|
|
|
pieces = File.basename(path).split(".")
|
|
|
|
pieces.shift
|
2010-12-01 05:22:48 -05:00
|
|
|
handler = Template.handler_for_extension(pieces.pop)
|
2010-10-10 03:24:17 -04:00
|
|
|
format = pieces.last && Mime[pieces.last]
|
|
|
|
[handler, format]
|
2009-04-23 18:58:38 -04:00
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-10 17:11:50 -04:00
|
|
|
# A resolver that loads files from the filesystem.
|
2009-09-02 18:00:22 -04:00
|
|
|
class FileSystemResolver < PathResolver
|
2011-03-19 00:13:59 -04:00
|
|
|
def initialize(path, pattern=nil)
|
2009-09-02 18:00:22 -04:00
|
|
|
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
|
2011-03-19 00:13:59 -04:00
|
|
|
super(pattern)
|
2010-06-23 19:04:41 -04:00
|
|
|
@path = File.expand_path(path)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
def to_s
|
|
|
|
@path.to_s
|
|
|
|
end
|
|
|
|
alias :to_path :to_s
|
|
|
|
|
2010-03-08 10:32:40 -05:00
|
|
|
def eql?(resolver)
|
|
|
|
self.class.equal?(resolver.class) && to_path == resolver.to_path
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2010-03-08 10:32:40 -05:00
|
|
|
alias :== :eql?
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2010-10-10 17:11:50 -04:00
|
|
|
|
|
|
|
# The same as FileSystemResolver but does not allow templates to store
|
|
|
|
# a virtual path since it is invalid for such resolvers.
|
|
|
|
class FallbackFileSystemResolver < FileSystemResolver
|
|
|
|
def self.instances
|
|
|
|
[new(""), new("/")]
|
|
|
|
end
|
|
|
|
|
|
|
|
def decorate(*)
|
|
|
|
super.each { |t| t.virtual_path = nil }
|
|
|
|
end
|
|
|
|
end
|
2009-12-28 19:28:26 -05:00
|
|
|
end
|