From 0d6e8edc2a47a4b4c6824936632bfb83850db343 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Sat, 4 May 2013 15:09:22 +0200 Subject: Move actionpack/lib/action_view* into actionview/lib --- actionview/lib/action_view/template/resolver.rb | 326 ++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 actionview/lib/action_view/template/resolver.rb (limited to 'actionview/lib/action_view/template/resolver.rb') diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb new file mode 100644 index 0000000000..3304605c1a --- /dev/null +++ b/actionview/lib/action_view/template/resolver.rb @@ -0,0 +1,326 @@ +require "pathname" +require "active_support/core_ext/class" +require "active_support/core_ext/class/attribute_accessors" +require "action_view/template" +require "thread" +require "thread_safe" + +module ActionView + # = Action View Resolver + class Resolver + # Keeps all information about view path and builds virtual path. + class Path + attr_reader :name, :prefix, :partial, :virtual + alias_method :partial?, :partial + + def self.build(name, prefix, partial) + virtual = "" + virtual << "#{prefix}/" unless prefix.empty? + virtual << (partial ? "_#{name}" : name) + new name, prefix, partial, virtual + end + + def initialize(name, prefix, partial, virtual) + @name = name + @prefix = prefix + @partial = partial + @virtual = virtual + end + + def to_str + @virtual + end + alias :to_s :to_str + end + + # Threadsafe template cache + class Cache #:nodoc: + class SmallCache < ThreadSafe::Cache + def initialize(options = {}) + super(options.merge(:initial_capacity => 2)) + end + end + + # preallocate all the default blocks for performance/memory consumption reasons + PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new} + PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)} + NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)} + KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)} + + # usually a majority of template look ups return nothing, use this canonical preallocated array to save memory + NO_TEMPLATES = [].freeze + + def initialize + @data = SmallCache.new(&KEY_BLOCK) + end + + # Cache the templates returned by the block + def cache(key, name, prefix, partial, locals) + if Resolver.caching? + @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield) + else + fresh_templates = yield + cached_templates = @data[key][name][prefix][partial][locals] + + if templates_have_changed?(cached_templates, fresh_templates) + @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates) + else + cached_templates || NO_TEMPLATES + end + end + end + + def clear + @data.clear + end + + private + + def canonical_no_templates(templates) + templates.empty? ? NO_TEMPLATES : templates + end + + def templates_have_changed?(cached_templates, fresh_templates) + # if either the old or new template list is empty, we don't need to (and can't) + # compare modification times, and instead just check whether the lists are different + if cached_templates.blank? || fresh_templates.blank? + return fresh_templates.blank? != cached_templates.blank? + end + + cached_templates_max_updated_at = cached_templates.map(&:updated_at).max + + # if a template has changed, it will be now be newer than all the cached templates + fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } + end + end + + cattr_accessor :caching + self.caching = true + + class << self + alias :caching? :caching + end + + def initialize + @cache = Cache.new + end + + def clear_cache + @cache.clear + end + + # Normalizes the arguments and passes it on to find_templates. + def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + cached(key, [name, prefix, partial], details, locals) do + find_templates(name, prefix, partial, details) + end + end + + private + + delegate :caching?, to: :class + + # This is what child classes implement. No defaults are needed + # because Resolver guarantees that the arguments are present and + # normalized. + def find_templates(name, prefix, partial, details) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" + end + + # Helpers that builds a path. Useful for building virtual paths. + def build_path(name, prefix, partial) + Path.build(name, prefix, partial) + end + + # Handles templates caching. If a key is given and caching is on + # always check the cache before hitting the resolver. Otherwise, + # it always hits the resolver but if the key is present, check if the + # resolver is fresher before returning it. + def cached(key, path_info, details, locals) #:nodoc: + name, prefix, partial = path_info + locals = locals.map { |x| x.to_s }.sort! + + if key + @cache.cache(key, name, prefix, partial, locals) do + decorate(yield, path_info, details, locals) + end + else + decorate(yield, path_info, details, locals) + 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)) + end + end + end + + # An abstract class that implements a Resolver with path semantics. + class PathResolver < Resolver #:nodoc: + EXTENSIONS = [:locale, :formats, :handlers] + DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}" + + def initialize(pattern=nil) + @pattern = pattern || DEFAULT_PATTERN + super() + end + + private + + def find_templates(name, prefix, partial, details) + path = Path.build(name, prefix, partial) + query(path, details, details[:formats]) + end + + def query(path, details, formats) + query = build_query(path, details) + + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } + + template_paths = Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } + + template_paths.map { |template| + handler, format = extract_handler_and_format(template, formats) + contents = File.binread template + + Template.new(contents, File.expand_path(template), handler, + :virtual_path => path.virtual, + :format => format, + :updated_at => mtime(template)) + } + end + + # Helper for building query glob string based on resolver's pattern. + def build_query(path, details) + query = @pattern.dup + + prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/\:prefix(\/)?/, prefix) + + partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) + query.gsub!(/\:action/, partial) + + details.each do |ext, variants| + query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + end + + File.expand_path(query, @path) + end + + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + end + + # Returns the file mtime from the filesystem. + def mtime(p) + File.mtime(p) + end + + # 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) + pieces = File.basename(path).split(".") + pieces.shift + + extension = pieces.pop + unless extension + message = "The file #{path} did not specify a template handler. The default is currently ERB, " \ + "but will change to RAW in the future." + ActiveSupport::Deprecation.warn message + end + + handler = Template.handler_for_extension(extension) + format = pieces.last && Template::Types[pieces.last] + [handler, format] + end + end + + # A resolver that loads files from the filesystem. It allows setting your own + # resolving pattern. Such pattern can be a glob string supported by some variables. + # + # ==== Examples + # + # Default pattern, loads views the same way as previous versions of rails, eg. when you're + # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}` + # + # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") + # + # This one allows you to keep files with different formats in separate subdirectories, + # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, + # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. + # + # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}") + # + # If you don't specify a pattern then the default will be used. + # + # In order to use any of the customized resolvers above in a Rails application, you just need + # to configure ActionController::Base.view_paths in an initializer, for example: + # + # ActionController::Base.view_paths = FileSystemResolver.new( + # Rails.root.join("app/views"), + # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" + # ) + # + # ==== Pattern format and variables + # + # Pattern has to be a valid glob string, and it allows you to use the + # following variables: + # + # * :prefix - usually the controller path + # * :action - name of the action + # * :locale - possible locale versions + # * :formats - possible request formats (for example html, json, xml...) + # * :handlers - possible handlers (for example erb, haml, builder...) + # + class FileSystemResolver < PathResolver + def initialize(path, pattern=nil) + raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) + super(pattern) + @path = File.expand_path(path) + end + + def to_s + @path.to_s + end + alias :to_path :to_s + + def eql?(resolver) + self.class.equal?(resolver.class) && to_path == resolver.to_path + end + alias :== :eql? + end + + # An Optimized resolver for Rails' most common case. + class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: + def build_query(path, details) + exts = EXTENSIONS.map { |ext| details[ext] } + query = escape_entry(File.join(@path, path)) + + query + exts.map { |ext| + "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" + }.join + end + end + + # The same as FileSystemResolver but does not allow templates to store + # a virtual path since it is invalid for such resolvers. + class FallbackFileSystemResolver < FileSystemResolver #:nodoc: + def self.instances + [new(""), new("/")] + end + + def decorate(*) + super.each { |t| t.virtual_path = nil } + end + end +end -- cgit v1.2.3