File: //usr/lib/ruby/vendor_ruby/tilt/mapping.rb
require 'monitor'
module Tilt
# Tilt::Mapping associates file extensions with template implementations.
#
# mapping = Tilt::Mapping.new
# mapping.register(Tilt::RDocTemplate, 'rdoc')
# mapping['index.rdoc'] # => Tilt::RDocTemplate
# mapping.new('index.rdoc').render
#
# You can use {#register} to register a template class by file
# extension, {#registered?} to see if a file extension is mapped,
# {#[]} to lookup template classes, and {#new} to instantiate template
# objects.
#
# Mapping also supports *lazy* template implementations. Note that regularly
# registered template implementations *always* have preference over lazily
# registered template implementations. You should use {#register} if you
# depend on a specific template implementation and {#register_lazy} if there
# are multiple alternatives.
#
# mapping = Tilt::Mapping.new
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
# mapping['index.md']
# # => RDiscount::Template
#
# {#register_lazy} takes a class name, a filename, and a list of file
# extensions. When you try to lookup a template name that matches the
# file extension, Tilt will automatically try to require the filename and
# constantize the class name.
#
# Unlike {#register}, there can be multiple template implementations
# registered lazily to the same file extension. Tilt will attempt to load the
# template implementations in order (registered *last* would be tried first),
# returning the first which doesn't raise LoadError.
#
# If all of the registered template implementations fails, Tilt will raise
# the exception of the first, since that was the most preferred one.
#
# mapping = Tilt::Mapping.new
# mapping.register_lazy('Bluecloth::Template', 'bluecloth/template', 'md')
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
# mapping['index.md']
# # => RDiscount::Template
#
# In the previous example we say that RDiscount has a *higher priority* than
# BlueCloth. Tilt will first try to `require "rdiscount/template"`, falling
# back to `require "bluecloth/template"`. If none of these are successful,
# the first error will be raised.
class Mapping
# @private
attr_reader :lazy_map, :template_map
def initialize
@template_map = Hash.new
@lazy_map = Hash.new { |h, k| h[k] = [] }
end
# @private
def initialize_copy(other)
@template_map = other.template_map.dup
@lazy_map = other.lazy_map.dup
end
# Registers a lazy template implementation by file extension. You
# can have multiple lazy template implementations defined on the
# same file extension, in which case the template implementation
# defined *last* will be attempted loaded *first*.
#
# @param class_name [String] Class name of a template class.
# @param file [String] Filename where the template class is defined.
# @param extensions [Array<String>] List of extensions.
# @return [void]
#
# @example
# mapping.register_lazy 'MyEngine::Template', 'my_engine/template', 'mt'
#
# defined?(MyEngine::Template) # => false
# mapping['index.mt'] # => MyEngine::Template
# defined?(MyEngine::Template) # => true
def register_lazy(class_name, file, *extensions)
# Internal API
if class_name.is_a?(Symbol)
Tilt.autoload class_name, file
class_name = "Tilt::#{class_name}"
end
extensions.each do |ext|
@lazy_map[ext].unshift([class_name, file])
end
end
# Registers a template implementation by file extension. There can only be
# one template implementation per file extension, and this method will
# override any existing mapping.
#
# @param template_class
# @param extensions [Array<String>] List of extensions.
# @return [void]
#
# @example
# mapping.register MyEngine::Template, 'mt'
# mapping['index.mt'] # => MyEngine::Template
def register(template_class, *extensions)
if template_class.respond_to?(:to_str)
# Support register(ext, template_class) too
extensions, template_class = [template_class], extensions[0]
end
extensions.each do |ext|
@template_map[ext.to_s] = template_class
end
end
# Checks if a file extension is registered (either eagerly or
# lazily) in this mapping.
#
# @param ext [String] File extension.
#
# @example
# mapping.registered?('erb') # => true
# mapping.registered?('nope') # => false
def registered?(ext)
@template_map.has_key?(ext.downcase) or lazy?(ext)
end
# Instantiates a new template class based on the file.
#
# @raise [RuntimeError] if there is no template class registered for the
# file name.
#
# @example
# mapping.new('index.mt') # => instance of MyEngine::Template
#
# @see Tilt::Template.new
def new(file, line=nil, options={}, &block)
if template_class = self[file]
template_class.new(file, line, options, &block)
else
fail "No template engine registered for #{File.basename(file)}"
end
end
# Looks up a template class based on file name and/or extension.
#
# @example
# mapping['views/hello.erb'] # => Tilt::ERBTemplate
# mapping['hello.erb'] # => Tilt::ERBTemplate
# mapping['erb'] # => Tilt::ERBTemplate
#
# @return [template class]
def [](file)
_, ext = split(file)
ext && lookup(ext)
end
alias template_for []
# Looks up a list of template classes based on file name. If the file name
# has multiple extensions, it will return all template classes matching the
# extensions from the end.
#
# @example
# mapping.templates_for('views/index.haml.erb')
# # => [Tilt::ERBTemplate, Tilt::HamlTemplate]
#
# @return [Array<template class>]
def templates_for(file)
templates = []
while true
prefix, ext = split(file)
break unless ext
templates << lookup(ext)
file = prefix
end
templates
end
# Finds the extensions the template class has been registered under.
# @param [template class] template_class
def extensions_for(template_class)
res = []
template_map.each do |ext, klass|
res << ext if template_class == klass
end
lazy_map.each do |ext, choices|
res << ext if choices.any? { |klass, file| template_class.to_s == klass }
end
res
end
private
def lazy?(ext)
ext = ext.downcase
@lazy_map.has_key?(ext) && !@lazy_map[ext].empty?
end
def split(file)
pattern = file.to_s.downcase
full_pattern = pattern.dup
until registered?(pattern)
return if pattern.empty?
pattern = File.basename(pattern)
pattern.sub!(/^[^.]*\.?/, '')
end
prefix_size = full_pattern.size - pattern.size
[full_pattern[0,prefix_size-1], pattern]
end
def lookup(ext)
@template_map[ext] || lazy_load(ext)
end
LOCK = Monitor.new
def lazy_load(pattern)
return unless @lazy_map.has_key?(pattern)
LOCK.enter
entered = true
choices = @lazy_map[pattern]
# Check if a template class is already present
choices.each do |class_name, file|
template_class = constant_defined?(class_name)
if template_class
register(template_class, pattern)
return template_class
end
end
first_failure = nil
# Load in order
choices.each do |class_name, file|
begin
require file
# It's safe to eval() here because constant_defined? will
# raise NameError on invalid constant names
template_class = eval(class_name)
rescue LoadError => ex
first_failure ||= ex
else
register(template_class, pattern)
return template_class
end
end
raise first_failure if first_failure
ensure
LOCK.exit if entered
end
# This is due to a bug in JRuby (see GH issue jruby/jruby#3585)
Tilt.autoload :Dummy, "tilt/dummy"
require "tilt/dummy"
AUTOLOAD_IS_BROKEN = Tilt.autoload?(:Dummy)
# The proper behavior (in MRI) for autoload? is to
# return `false` when the constant/file has been
# explicitly required.
#
# However, in JRuby it returns `true` even after it's
# been required. In that case it turns out that `defined?`
# returns `"constant"` if it exists and `nil` when it doesn't.
# This is actually a second bug: `defined?` should resolve
# autoload (aka. actually try to require the file).
#
# We use the second bug in order to resolve the first bug.
def constant_defined?(name)
name.split('::').inject(Object) do |scope, n|
if scope.autoload?(n)
if !AUTOLOAD_IS_BROKEN
return false
end
if eval("!defined?(scope::#{n})")
return false
end
end
return false if !scope.const_defined?(n)
scope.const_get(n)
end
end
end
end