diff options
Diffstat (limited to 'actionview/lib/action_view/template')
-rw-r--r-- | actionview/lib/action_view/template/error.rb | 138 | ||||
-rw-r--r-- | actionview/lib/action_view/template/handlers.rb | 53 | ||||
-rw-r--r-- | actionview/lib/action_view/template/handlers/builder.rb | 26 | ||||
-rw-r--r-- | actionview/lib/action_view/template/handlers/erb.rb | 145 | ||||
-rw-r--r-- | actionview/lib/action_view/template/handlers/raw.rb | 11 | ||||
-rw-r--r-- | actionview/lib/action_view/template/resolver.rb | 326 | ||||
-rw-r--r-- | actionview/lib/action_view/template/text.rb | 34 | ||||
-rw-r--r-- | actionview/lib/action_view/template/types.rb | 57 |
8 files changed, 790 insertions, 0 deletions
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb new file mode 100644 index 0000000000..a89d51221e --- /dev/null +++ b/actionview/lib/action_view/template/error.rb @@ -0,0 +1,138 @@ +require "active_support/core_ext/enumerable" + +module ActionView + # = Action View Errors + class ActionViewError < StandardError #:nodoc: + end + + class EncodingError < StandardError #:nodoc: + end + + class MissingRequestError < StandardError #:nodoc: + end + + class WrongEncodingError < EncodingError #:nodoc: + def initialize(string, encoding) + @string, @encoding = string, encoding + end + + def message + @string.force_encoding(Encoding::ASCII_8BIT) + "Your template was not saved as valid #{@encoding}. Please " \ + "either specify #{@encoding} as the encoding for your template " \ + "in your text editor, or mark the template with its " \ + "encoding by inserting the following as the first line " \ + "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \ + "The source of your template was:\n\n#{@string}" + end + end + + class MissingTemplate < ActionViewError #:nodoc: + attr_reader :path + + def initialize(paths, path, prefixes, partial, details, *) + @path = path + prefixes = Array(prefixes) + template_type = if partial + "partial" + elsif path =~ /layouts/i + 'layout' + else + 'template' + end + + searched_paths = prefixes.map { |prefix| [prefix, path].join("/") } + + out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n" + out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join + super out + end + end + + class Template + # The Template::Error exception is raised when the compilation or rendering of the template + # fails. This exception then gathers a bunch of intimate details and uses it to report a + # precise exception message. + class Error < ActionViewError #:nodoc: + SOURCE_CODE_RADIUS = 3 + + attr_reader :original_exception, :backtrace + + def initialize(template, original_exception) + super(original_exception.message) + @template, @original_exception = template, original_exception + @sub_templates = nil + @backtrace = original_exception.backtrace + end + + def file_name + @template.identifier + end + + def sub_template_message + if @sub_templates + "Trace of template inclusion: " + + @sub_templates.collect { |template| template.inspect }.join(", ") + else + "" + end + end + + def source_extract(indentation = 0, output = :console) + 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 = end_on_line.to_s.size + indentation + return unless source_code = source_code[start_on_line..end_on_line] + + formatted_code_for(source_code, start_on_line, indent, output) + 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 || backtrace.find { |line| line =~ regexp } + end + end + + def annoted_source_code + source_extract(4) + end + + private + + def source_location + if line_number + "on line ##{line_number} of " + else + 'in ' + end + file_name + end + + def formatted_code_for(source_code, line_counter, indent, output) + start_value = (output == :html) ? {} : "" + source_code.inject(start_value) do |result, line| + line_counter += 1 + if output == :html + result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line]) + else + result << "%#{indent}s: %s\n" % [line_counter, line] + end + end + end + end + end + + TemplateError = Template::Error +end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb new file mode 100644 index 0000000000..d9cddc0040 --- /dev/null +++ b/actionview/lib/action_view/template/handlers.rb @@ -0,0 +1,53 @@ +module ActionView #:nodoc: + # = Action View Template Handlers + class Template + module Handlers #:nodoc: + autoload :ERB, 'action_view/template/handlers/erb' + autoload :Builder, 'action_view/template/handlers/builder' + autoload :Raw, 'action_view/template/handlers/raw' + + def self.extended(base) + base.register_default_template_handler :erb, ERB.new + base.register_template_handler :builder, Builder.new + base.register_template_handler :raw, Raw.new + base.register_template_handler :ruby, :source.to_proc + end + + @@template_handlers = {} + @@default_template_handlers = nil + + def self.extensions + @@template_extensions ||= @@template_handlers.keys + end + + # Register an object that knows how to handle template files with the given + # extensions. This can be used to implement new template types. + # The handler must respond to `:call`, which will be passed the template + # and should return the rendered template as a String. + def register_template_handler(*extensions, handler) + raise(ArgumentError, "Extension is required") if extensions.empty? + extensions.each do |extension| + @@template_handlers[extension.to_sym] = handler + end + @@template_extensions = nil + end + + def template_handler_extensions + @@template_handlers.keys.map {|key| key.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_for_extension(extension) + registered_template_handler(extension) || @@default_template_handlers + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb new file mode 100644 index 0000000000..d90b0c6378 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -0,0 +1,26 @@ +module ActionView + module Template::Handlers + class Builder + # Default format used by Builder. + class_attribute :default_format + self.default_format = :xml + + def call(template) + require_engine + "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "self.output_buffer = xml.target!;" + + template.source + + ";xml.target!;" + end + + protected + + def require_engine + @required ||= begin + require "builder" + true + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb new file mode 100644 index 0000000000..c8a0059596 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -0,0 +1,145 @@ +require 'erubis' + +module ActionView + class Template + module Handlers + class Erubis < ::Erubis::Eruby + def add_preamble(src) + @newline_pending = 0 + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + end + + def add_text(src, text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << escape_text(text) + src << "';" + + @newline_pending = 0 + end + end + + # Erubis toggles <%= and <%== behavior when escaping is enabled. + # We override to always treat <%== as escaped. + def add_expr(src, code, indicator) + case indicator + when '==' + add_expr_escaped(src, code) + else + super + end + end + + BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expr_literal(src, code) + flush_newline_if_pending(src) + if code =~ BLOCK_EXPR + src << '@output_buffer.append= ' << code + else + src << '@output_buffer.append=(' << code << ');' + end + end + + def add_expr_escaped(src, code) + flush_newline_if_pending(src) + if code =~ BLOCK_EXPR + src << "@output_buffer.safe_append= " << code + else + src << "@output_buffer.safe_append=(" << code << ");" + end + end + + def add_stmt(src, code) + flush_newline_if_pending(src) + super + end + + def add_postamble(src) + flush_newline_if_pending(src) + src << '@output_buffer.to_s' + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';" + @newline_pending = 0 + end + end + end + + class ERB + # Specify trim mode for the ERB compiler. Defaults to '-'. + # See ERB documentation for suitable values. + class_attribute :erb_trim_mode + self.erb_trim_mode = '-' + + # Default implementation used. + class_attribute :erb_implementation + self.erb_implementation = Erubis + + # Do not escape templates of these mime types. + class_attribute :escape_whitelist + self.escape_whitelist = ["text/plain"] + + ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") + + def self.call(template) + new.call(template) + end + + def supports_streaming? + true + end + + def handles_encoding? + true + end + + def call(template) + # First, convert to BINARY, so in case the encoding is + # wrong, we can still find an encoding tag + # (<%# encoding %>) inside the String using a regular + # expression + template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) + + erb = template_source.gsub(ENCODING_TAG, '') + encoding = $2 + + erb.force_encoding valid_encoding(template.source.dup, encoding) + + # Always make sure we return a String in the default_internal + erb.encode! + + self.class.erb_implementation.new( + erb, + :escape => (self.class.escape_whitelist.include? template.type), + :trim => (self.class.erb_trim_mode == "-") + ).src + end + + private + + def valid_encoding(string, encoding) + # If a magic encoding comment was found, tag the + # String with this encoding. This is for a case + # where the original String was assumed to be, + # for instance, UTF-8, but a magic comment + # proved otherwise + string.force_encoding(encoding) if encoding + + # If the String is valid, return the encoding we found + return string.encoding if string.valid_encoding? + + # Otherwise, raise an exception + raise WrongEncodingError.new(string, string.encoding) + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb new file mode 100644 index 0000000000..0c0d1fffcb --- /dev/null +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -0,0 +1,11 @@ +module ActionView + module Template::Handlers + class Raw + def call(template) + escaped = template.source.gsub(':', '\:') + + '%q:' + escaped + ':;' + end + end + end +end 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: + # + # * <tt>:prefix</tt> - usually the controller path + # * <tt>:action</tt> - name of the action + # * <tt>:locale</tt> - possible locale versions + # * <tt>:formats</tt> - possible request formats (for example html, json, xml...) + # * <tt>:handlers</tt> - 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 diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb new file mode 100644 index 0000000000..859c7bc3ce --- /dev/null +++ b/actionview/lib/action_view/template/text.rb @@ -0,0 +1,34 @@ +module ActionView #:nodoc: + # = Action View Text Template + class Template + class Text #:nodoc: + attr_accessor :type + + def initialize(string, type = nil) + @string = string.to_s + @type = Types[type] || type if type + @type ||= Types[:text] + end + + def identifier + 'text template' + end + + def inspect + 'text template' + end + + def to_str + @string + end + + def render(*args) + to_str + end + + def formats + [@type.to_sym] + end + end + end +end diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb new file mode 100644 index 0000000000..db77cb5d19 --- /dev/null +++ b/actionview/lib/action_view/template/types.rb @@ -0,0 +1,57 @@ +require 'set' +require 'active_support/core_ext/class/attribute_accessors' + +module ActionView + class Template + class Types + class Type + cattr_accessor :types + self.types = Set.new + + def self.register(*t) + types.merge(t.map { |type| type.to_s }) + end + + register :html, :text, :js, :css, :xml, :json + + def self.[](type) + return type if type.is_a?(self) + + if type.is_a?(Symbol) || types.member?(type.to_s) + new(type) + end + end + + attr_reader :symbol + + def initialize(symbol) + @symbol = symbol.to_sym + end + + delegate :to_s, :to_sym, :to => :symbol + alias to_str to_s + + def ref + to_sym || to_s + end + + def ==(type) + return false if type.blank? + symbol.to_sym == type.to_sym + end + end + + cattr_accessor :type_klass + + def self.delegate_to(klass) + self.type_klass = klass + end + + delegate_to Type + + def self.[](type) + type_klass[type] + end + end + end +end |