aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/template
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_view/template')
-rw-r--r--actionpack/lib/action_view/template/error.rb99
-rw-r--r--actionpack/lib/action_view/template/handler.rb34
-rw-r--r--actionpack/lib/action_view/template/handlers.rb48
-rw-r--r--actionpack/lib/action_view/template/handlers/builder.rb17
-rw-r--r--actionpack/lib/action_view/template/handlers/erb.rb22
-rw-r--r--actionpack/lib/action_view/template/handlers/rjs.rb13
-rw-r--r--actionpack/lib/action_view/template/inline.rb19
-rw-r--r--actionpack/lib/action_view/template/partial.rb18
-rw-r--r--actionpack/lib/action_view/template/renderable.rb74
-rw-r--r--actionpack/lib/action_view/template/template.rb241
10 files changed, 585 insertions, 0 deletions
diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb
new file mode 100644
index 0000000000..37cb1c7c6c
--- /dev/null
+++ b/actionpack/lib/action_view/template/error.rb
@@ -0,0 +1,99 @@
+module ActionView
+ # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a
+ # bunch of intimate details and uses it to report a very precise exception message.
+ class TemplateError < ActionViewError #:nodoc:
+ SOURCE_CODE_RADIUS = 3
+
+ attr_reader :original_exception
+
+ def initialize(template, assigns, original_exception)
+ @template, @assigns, @original_exception = template, assigns.dup, original_exception
+ @backtrace = compute_backtrace
+ end
+
+ def file_name
+ @template.relative_path
+ end
+
+ def message
+ ActiveSupport::Deprecation.silence { original_exception.message }
+ end
+
+ def clean_backtrace
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
+ Rails.backtrace_cleaner.clean(original_exception.backtrace)
+ else
+ original_exception.backtrace
+ end
+ end
+
+ def sub_template_message
+ if @sub_templates
+ "Trace of template inclusion: " +
+ @sub_templates.collect { |template| template.relative_path }.join(", ")
+ else
+ ""
+ end
+ end
+
+ def source_extract(indentation = 0)
+ return unless num = line_number
+ num = num.to_i
+
+ source_code = @template.source.split("\n")
+
+ start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
+ end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
+
+ indent = ' ' * indentation
+ line_counter = start_on_line
+ return unless source_code = source_code[start_on_line..end_on_line]
+
+ source_code.sum do |line|
+ line_counter += 1
+ "#{indent}#{line_counter}: #{line}\n"
+ end
+ end
+
+ def sub_template_of(template_path)
+ @sub_templates ||= []
+ @sub_templates << template_path
+ end
+
+ def line_number
+ @line_number ||=
+ if file_name
+ regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
+
+ $1 if message =~ regexp or clean_backtrace.find { |line| line =~ regexp }
+ end
+ end
+
+ def to_s
+ "\n#{self.class} (#{message}) #{source_location}:\n" +
+ "#{source_extract}\n #{clean_backtrace.join("\n ")}\n\n"
+ end
+
+ # don't do anything nontrivial here. Any raised exception from here becomes fatal
+ # (and can't be rescued).
+ def backtrace
+ @backtrace
+ end
+
+ private
+ def compute_backtrace
+ [
+ "#{source_location.capitalize}\n\n#{source_extract(4)}\n " +
+ clean_backtrace.join("\n ")
+ ]
+ end
+
+ def source_location
+ if line_number
+ "on line ##{line_number} of "
+ else
+ 'in '
+ end + file_name
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/template/handler.rb b/actionpack/lib/action_view/template/handler.rb
new file mode 100644
index 0000000000..672da0ed2b
--- /dev/null
+++ b/actionpack/lib/action_view/template/handler.rb
@@ -0,0 +1,34 @@
+# Legacy TemplateHandler stub
+module ActionView
+ module TemplateHandlers #:nodoc:
+ module Compilable
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def call(template)
+ new.compile(template)
+ end
+ end
+
+ def compile(template)
+ raise "Need to implement #{self.class.name}#compile(template)"
+ end
+ end
+ end
+
+ class TemplateHandler
+ def self.call(template)
+ "#{name}.new(self).render(template, local_assigns)"
+ end
+
+ def initialize(view = nil)
+ @view = view
+ end
+
+ def render(template, local_assigns)
+ raise "Need to implement #{self.class.name}#render(template, local_assigns)"
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb
new file mode 100644
index 0000000000..fb85f28851
--- /dev/null
+++ b/actionpack/lib/action_view/template/handlers.rb
@@ -0,0 +1,48 @@
+module ActionView #:nodoc:
+ module TemplateHandlers #:nodoc:
+ autoload :ERB, 'action_view/template/handlers/erb'
+ autoload :RJS, 'action_view/template/handlers/rjs'
+ autoload :Builder, 'action_view/template/handlers/builder'
+
+ def self.extended(base)
+ base.register_default_template_handler :erb, TemplateHandlers::ERB
+ base.register_template_handler :rjs, TemplateHandlers::RJS
+ base.register_template_handler :builder, TemplateHandlers::Builder
+
+ # TODO: Depreciate old template extensions
+ base.register_template_handler :rhtml, TemplateHandlers::ERB
+ base.register_template_handler :rxml, TemplateHandlers::Builder
+ end
+
+ @@template_handlers = {}
+ @@default_template_handlers = nil
+
+ # Register a class that knows how to handle template files with the given
+ # extension. This can be used to implement new template types.
+ # The constructor for the class must take the ActiveView::Base instance
+ # as a parameter, and the class must implement a +render+ method that
+ # takes the contents of the template to render as well as the Hash of
+ # local assigns available to the template. The +render+ method ought to
+ # return the rendered template as a string.
+ def register_template_handler(extension, klass)
+ @@template_handlers[extension.to_sym] = klass
+ end
+
+ def template_handler_extensions
+ @@template_handlers.keys.map(&:to_s).sort
+ end
+
+ def registered_template_handler(extension)
+ extension && @@template_handlers[extension.to_sym]
+ end
+
+ def register_default_template_handler(extension, klass)
+ register_template_handler(extension, klass)
+ @@default_template_handlers = klass
+ end
+
+ def handler_class_for_extension(extension)
+ registered_template_handler(extension) || @@default_template_handlers
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionpack/lib/action_view/template/handlers/builder.rb
new file mode 100644
index 0000000000..788dc93326
--- /dev/null
+++ b/actionpack/lib/action_view/template/handlers/builder.rb
@@ -0,0 +1,17 @@
+require 'builder'
+
+module ActionView
+ module TemplateHandlers
+ class Builder < TemplateHandler
+ include Compilable
+
+ def compile(template)
+ "_set_controller_content_type(Mime::XML);" +
+ "xml = ::Builder::XmlMarkup.new(:indent => 2);" +
+ "self.output_buffer = xml.target!;" +
+ template.source +
+ ";xml.target!;"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb
new file mode 100644
index 0000000000..e3120ba267
--- /dev/null
+++ b/actionpack/lib/action_view/template/handlers/erb.rb
@@ -0,0 +1,22 @@
+module ActionView
+ module TemplateHandlers
+ class ERB < TemplateHandler
+ include Compilable
+
+ ##
+ # :singleton-method:
+ # Specify trim mode for the ERB compiler. Defaults to '-'.
+ # See ERb documentation for suitable values.
+ cattr_accessor :erb_trim_mode
+ self.erb_trim_mode = '-'
+
+ def compile(template)
+ src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src
+
+ # Ruby 1.9 prepends an encoding to the source. However this is
+ # useless because you can only set an encoding on the first line
+ RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/handlers/rjs.rb b/actionpack/lib/action_view/template/handlers/rjs.rb
new file mode 100644
index 0000000000..802a79b3fc
--- /dev/null
+++ b/actionpack/lib/action_view/template/handlers/rjs.rb
@@ -0,0 +1,13 @@
+module ActionView
+ module TemplateHandlers
+ class RJS < TemplateHandler
+ include Compilable
+
+ def compile(template)
+ "@formats = [:html];" +
+ "controller.response.content_type ||= Mime::JS;" +
+ "update_page do |page|;#{template.source}\nend"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/inline.rb b/actionpack/lib/action_view/template/inline.rb
new file mode 100644
index 0000000000..54efa543c8
--- /dev/null
+++ b/actionpack/lib/action_view/template/inline.rb
@@ -0,0 +1,19 @@
+module ActionView #:nodoc:
+ class InlineTemplate #:nodoc:
+ include Renderable
+
+ attr_reader :source, :extension, :method_segment
+
+ def initialize(source, type = nil)
+ @source = source
+ @extension = type
+ @method_segment = "inline_#{@source.hash.abs}"
+ end
+
+ private
+ # Always recompile inline templates
+ def recompile?
+ true
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/partial.rb b/actionpack/lib/action_view/template/partial.rb
new file mode 100644
index 0000000000..30dec1dc5b
--- /dev/null
+++ b/actionpack/lib/action_view/template/partial.rb
@@ -0,0 +1,18 @@
+module ActionView
+ # NOTE: The template that this mixin is being included into is frozen
+ # so you cannot set or modify any instance variables
+ module RenderablePartial #:nodoc:
+ extend ActiveSupport::Memoizable
+
+ def variable_name
+ name.sub(/\A_/, '').to_sym
+ end
+ memoize :variable_name
+
+ def counter_name
+ "#{variable_name}_counter".to_sym
+ end
+ memoize :counter_name
+
+ end
+end
diff --git a/actionpack/lib/action_view/template/renderable.rb b/actionpack/lib/action_view/template/renderable.rb
new file mode 100644
index 0000000000..35c832aaba
--- /dev/null
+++ b/actionpack/lib/action_view/template/renderable.rb
@@ -0,0 +1,74 @@
+module ActionView
+ # NOTE: The template that this mixin is being included into is frozen
+ # so you cannot set or modify any instance variables
+ module Renderable #:nodoc:
+ extend ActiveSupport::Memoizable
+
+ def filename
+ 'compiled-template'
+ end
+
+ def handler
+ Template.handler_class_for_extension(extension)
+ end
+ memoize :handler
+
+ def compiled_source
+ handler.call(self)
+ end
+ memoize :compiled_source
+
+ def method_name_without_locals
+ ['_run', extension, method_segment].compact.join('_')
+ end
+ memoize :method_name_without_locals
+
+ def method_name(local_assigns)
+ if local_assigns && local_assigns.any?
+ method_name = method_name_without_locals.dup
+ method_name << "_locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
+ else
+ method_name = method_name_without_locals
+ end
+ method_name.to_sym
+ end
+
+ # Compile and evaluate the template's code (if necessary)
+ def compile(local_assigns)
+ render_symbol = method_name(local_assigns)
+
+ if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
+ compile!(render_symbol, local_assigns)
+ end
+ end
+
+ private
+ def compile!(render_symbol, local_assigns)
+ locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
+
+ source = <<-end_src
+ def #{render_symbol}(local_assigns)
+ old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
+ ensure
+ self.output_buffer = old_output_buffer
+ end
+ end_src
+
+ begin
+ ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
+ rescue Exception => e # errors from template code
+ if logger = defined?(ActionController) && Base.logger
+ logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
+ logger.debug "Function body: #{source}"
+ logger.debug "Backtrace: #{e.backtrace.join("\n")}"
+ end
+
+ raise ActionView::TemplateError.new(self, {}, e)
+ end
+ end
+
+ def recompile?
+ false
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb
new file mode 100644
index 0000000000..235a95a0f3
--- /dev/null
+++ b/actionpack/lib/action_view/template/template.rb
@@ -0,0 +1,241 @@
+module ActionView #:nodoc:
+ class Template
+ class Path
+ attr_reader :path, :paths
+ delegate :hash, :inspect, :to => :path
+
+ def initialize(path)
+ raise ArgumentError, "path already is a Path class" if path.is_a?(Path)
+ @path = path.freeze
+ end
+
+ def to_s
+ if defined?(RAILS_ROOT)
+ path.to_s.sub(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}\//, '')
+ else
+ path.to_s
+ end
+ end
+
+ def to_str
+ path.to_str
+ end
+
+ def ==(path)
+ to_str == path.to_str
+ end
+
+ def eql?(path)
+ to_str == path.to_str
+ end
+
+ # Returns a ActionView::Template object for the given path string. The
+ # input path should be relative to the view path directory,
+ # +hello/index.html.erb+. This method also has a special exception to
+ # match partial file names without a handler extension. So
+ # +hello/index.html+ will match the first template it finds with a
+ # known template extension, +hello/index.html.erb+. Template extensions
+ # should not be confused with format extensions +html+, +js+, +xml+,
+ # etc. A format must be supplied to match a formated file. +hello/index+
+ # will never match +hello/index.html.erb+.
+ def find_template(path)
+ templates_in_path do |template|
+ if template.accessible_paths.include?(path)
+ return template
+ end
+ end
+ nil
+ end
+
+ def find_by_parts(name, extensions = nil, prefix = nil, partial = nil)
+ path = prefix ? "#{prefix}/" : ""
+
+ name = name.split("/")
+ name[-1] = "_#{name[-1]}" if partial
+
+ path << name.join("/")
+
+ template = nil
+
+ Array(extensions).each do |extension|
+ extensioned_path = extension ? "#{path}.#{extension}" : path
+ template = find_template(extensioned_path) || find_template(path)
+ break if template
+ end
+ template
+ end
+
+ private
+ def templates_in_path
+ (Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
+ yield create_template(file) unless File.directory?(file)
+ end
+ end
+
+ def create_template(file)
+ Template.new(file.split("#{self}/").last, self)
+ end
+ end
+
+ class EagerPath < Path
+ def initialize(path)
+ super
+
+ @paths = {}
+ templates_in_path do |template|
+ template.load!
+ template.accessible_paths.each do |path|
+ @paths[path] = template
+ end
+ end
+ @paths.freeze
+ end
+
+ def find_template(path)
+ @paths[path]
+ end
+ end
+
+ extend TemplateHandlers
+ extend ActiveSupport::Memoizable
+ include Renderable
+
+ # Templates that are exempt from layouts
+ @@exempt_from_layout = Set.new([/\.rjs$/])
+
+ # Don't render layouts for templates with the given extensions.
+ def self.exempt_from_layout(*extensions)
+ regexps = extensions.collect do |extension|
+ extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
+ end
+ @@exempt_from_layout.merge(regexps)
+ end
+
+ attr_accessor :filename, :load_path, :base_path, :name, :format, :extension
+ delegate :to_s, :to => :path
+
+ def initialize(template_path, load_paths = [])
+ template_path = template_path.dup
+ @load_path, @filename = find_full_path(template_path, load_paths)
+ @base_path, @name, @format, @extension = split(template_path)
+ @base_path.to_s.gsub!(/\/$/, '') # Push to split method
+
+ # Extend with partial super powers
+ extend RenderablePartial if @name =~ /^_/
+ end
+
+ def accessible_paths
+ paths = []
+ paths << path
+ paths << path_without_extension
+ if multipart?
+ formats = format.split(".")
+ paths << "#{path_without_format_and_extension}.#{formats.first}"
+ paths << "#{path_without_format_and_extension}.#{formats.second}"
+ end
+ paths
+ end
+
+ def format_and_extension
+ (extensions = [format, extension].compact.join(".")).blank? ? nil : extensions
+ end
+ memoize :format_and_extension
+
+ def multipart?
+ format && format.include?('.')
+ end
+
+ def content_type
+ format.gsub('.', '/')
+ end
+
+ def mime_type
+ Mime::Type.lookup_by_extension(format) if format
+ end
+ memoize :mime_type
+
+ def path
+ [base_path, [name, format, extension].compact.join('.')].compact.join('/')
+ end
+ memoize :path
+
+ def path_without_extension
+ [base_path, [name, format].compact.join('.')].compact.join('/')
+ end
+ memoize :path_without_extension
+
+ def path_without_format_and_extension
+ [base_path, name].compact.join('/')
+ end
+ memoize :path_without_format_and_extension
+
+ def relative_path
+ path = File.expand_path(filename)
+ path.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}\//, '') if defined?(RAILS_ROOT)
+ path
+ end
+ memoize :relative_path
+
+ def exempt_from_layout?
+ @@exempt_from_layout.any? { |exempted| path =~ exempted }
+ end
+
+ def mtime
+ File.mtime(filename)
+ end
+ memoize :mtime
+
+ def source
+ File.read(filename)
+ end
+ memoize :source
+
+ def method_segment
+ relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }
+ end
+ memoize :method_segment
+
+ def stale?
+ File.mtime(filename) > mtime
+ end
+
+ def recompile?
+ !@cached
+ end
+
+ def load!
+ @cached = true
+ freeze
+ end
+
+ private
+ def valid_extension?(extension)
+ !Template.registered_template_handler(extension).nil?
+ end
+
+ def find_full_path(path, load_paths)
+ load_paths = Array(load_paths) + [nil]
+ load_paths.each do |load_path|
+ file = load_path ? "#{load_path.to_str}/#{path}" : path
+ return load_path, file if File.file?(file)
+ end
+ raise MissingTemplate.new(load_paths, path)
+ end
+
+ # Returns file split into an array
+ # [base_path, name, format, extension]
+ def split(file)
+ if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/)
+ if valid_extension?(m[5]) # Multipart formats
+ [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]]
+ elsif valid_extension?(m[4]) # Single format
+ [m[1], m[2], m[3], m[4]]
+ elsif valid_extension?(m[3]) # No format
+ [m[1], m[2], nil, m[3]]
+ else # No extension
+ [m[1], m[2], m[3], nil]
+ end
+ end
+ end
+ end
+end