diff options
Diffstat (limited to 'actionpack/lib')
42 files changed, 816 insertions, 679 deletions
diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb index c5fc6c7966..3deda0b45a 100644 --- a/actionpack/lib/action_controller/assertions/response_assertions.rb +++ b/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -97,7 +97,7 @@ module ActionController value['controller'] = value['controller'].to_s if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) - value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) + value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) && @response.redirected_to.is_a?(Hash) end value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 6222c3205d..817b270feb 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -283,13 +283,6 @@ module ActionController #:nodoc: @@debug_routes = true cattr_accessor :debug_routes - # Indicates to Mongrel or Webrick whether to allow concurrent action - # processing. Your controller actions and any other code they call must - # also behave well when called from concurrent threads. Turned off by - # default. - @@allow_concurrency = false - cattr_accessor :allow_concurrency - # Modern REST web services often need to submit complex data to the web application. # The <tt>@@param_parsers</tt> hash lets you register handlers which will process the HTTP body and add parameters to the # <tt>params</tt> hash. These handlers are invoked for POST and PUT requests. @@ -428,8 +421,7 @@ module ActionController #:nodoc: end def view_paths=(value) - @view_paths = value - ActionView::TemplateFinder.process_view_paths(value) + @view_paths = ActionView::ViewLoadPaths.new(Array(value)) if value end # Adds a view_path to the front of the view_paths array. @@ -441,8 +433,7 @@ module ActionController #:nodoc: # def prepend_view_path(path) @view_paths = superclass.view_paths.dup if @view_paths.nil? - view_paths.unshift(*path) - ActionView::TemplateFinder.process_view_paths(path) + @view_paths.unshift(*path) end # Adds a view_path to the end of the view_paths array. @@ -454,8 +445,7 @@ module ActionController #:nodoc: # def append_view_path(path) @view_paths = superclass.view_paths.dup if @view_paths.nil? - view_paths.push(*path) - ActionView::TemplateFinder.process_view_paths(path) + @view_paths.push(*path) end # Replace sensitive parameter data from the request log. @@ -613,8 +603,8 @@ module ActionController #:nodoc: # # This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt> # would have slashed-off the path components after the changed action. - def url_for(options = nil) #:doc: - case options || {} + def url_for(options = {}) #:doc: + case options when String options when Hash @@ -647,11 +637,11 @@ module ActionController #:nodoc: # View load paths for controller. def view_paths - @template.finder.view_paths + @template.view_paths end def view_paths=(value) - @template.finder.view_paths = value # Mutex needed + @template.view_paths = ViewLoadPaths.new(value) end # Adds a view_path to the front of the view_paths array. @@ -661,7 +651,7 @@ module ActionController #:nodoc: # self.prepend_view_path(["views/default", "views/custom"]) # def prepend_view_path(path) - @template.finder.prepend_view_path(path) # Mutex needed + @template.view_paths.unshift(*path) end # Adds a view_path to the end of the view_paths array. @@ -671,7 +661,7 @@ module ActionController #:nodoc: # self.append_view_path(["views/default", "views/custom"]) # def append_view_path(path) - @template.finder.append_view_path(path) # Mutex needed + @template.view_paths.push(*path) end protected @@ -1232,7 +1222,7 @@ module ActionController #:nodoc: end def template_exists?(template_name = default_template_name) - @template.finder.file_exists?(template_name) + @template.file_exists?(template_name) end def template_public?(template_name = default_template_name) @@ -1240,9 +1230,8 @@ module ActionController #:nodoc: end def template_exempt_from_layout?(template_name = default_template_name) - extension = @template && @template.finder.pick_template_extension(template_name) - name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name - @@exempt_from_layout.any? { |ext| name_with_extension =~ ext } + template_name = @template.send(:template_file_from_name, template_name) if @template + @@exempt_from_layout.any? { |ext| template_name.to_s =~ ext } end def default_template_name(action_name = self.action_name) diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 1ef9e60a21..65a36f7f98 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -40,6 +40,8 @@ module ActionController #:nodoc: # controller.send(:list_url, c.params[:id]) } # end # + # If you pass :layout => false, it will only cache your action content. It is useful when your layout has dynamic information. + # module Actions def self.included(base) #:nodoc: base.extend(ClassMethods) @@ -54,7 +56,8 @@ module ActionController #:nodoc: def caches_action(*actions) return unless cache_configured? options = actions.extract_options! - around_filter(ActionCacheFilter.new(:cache_path => options.delete(:cache_path)), {:only => actions}.merge(options)) + cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path)) + around_filter(cache_filter, {:only => actions}.merge(options)) end end @@ -64,10 +67,10 @@ module ActionController #:nodoc: if options[:action].is_a?(Array) options[:action].dup.each do |action| - expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }))) + expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }), false)) end else - expire_fragment(ActionCachePath.path_for(self, options)) + expire_fragment(ActionCachePath.path_for(self, options, false)) end end @@ -81,7 +84,9 @@ module ActionController #:nodoc: if cache = controller.read_fragment(cache_path.path) controller.rendered_action_cache = true set_content_type!(controller, cache_path.extension) - controller.send!(:render_for_text, cache) + options = { :text => cache } + options.merge!(:layout => true) if cache_layout? + controller.send!(:render, options) false else controller.action_cache_path = cache_path @@ -90,7 +95,8 @@ module ActionController #:nodoc: def after(controller) return if controller.rendered_action_cache || !caching_allowed(controller) - controller.write_fragment(controller.action_cache_path.path, controller.response.body) + action_content = cache_layout? ? content_for_layout(controller) : controller.response.body + controller.write_fragment(controller.action_cache_path.path, action_content) end private @@ -105,22 +111,38 @@ module ActionController #:nodoc: def caching_allowed(controller) controller.request.get? && controller.response.headers['Status'].to_i == 200 end + + def cache_layout? + @options[:layout] == false + end + + def content_for_layout(controller) + controller.response.layout && controller.response.template.instance_variable_get('@content_for_layout') + end end class ActionCachePath attr_reader :path, :extension class << self - def path_for(controller, options) - new(controller, options).path + def path_for(controller, options, infer_extension=true) + new(controller, options, infer_extension).path end end - - def initialize(controller, options = {}) - @extension = extract_extension(controller.request.path) + + # When true, infer_extension will look up the cache path extension from the request's path & format. + # This is desirable when reading and writing the cache, but not when expiring the cache - expire_action should expire the same files regardless of the request format. + def initialize(controller, options = {}, infer_extension=true) + if infer_extension and options.is_a? Hash + request_extension = extract_extension(controller.request) + options = options.reverse_merge(:format => request_extension) + end path = controller.url_for(options).split('://').last normalize!(path) - add_extension!(path, @extension) + if infer_extension + @extension = request_extension + add_extension!(path, @extension) + end @path = URI.unescape(path) end @@ -130,13 +152,22 @@ module ActionController #:nodoc: end def add_extension!(path, extension) - path << ".#{extension}" if extension + path << ".#{extension}" if extension and !path.ends_with?(extension) end - - def extract_extension(file_path) + + def extract_extension(request) # Don't want just what comes after the last '.' to accommodate multi part extensions # such as tar.gz. - file_path[/^[^.]+\.(.+)$/, 1] + extension = request.path[/^[^.]+\.(.+)$/, 1] + + # If there's no extension in the path, check request.format + if extension.nil? + extension = request.format.to_sym.to_s + if extension=='all' + extension = nil + end + end + extension end end end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 578e031a17..45946421fc 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -61,16 +61,18 @@ module ActionController #:nodoc: end def fragment_for(block, name = {}, options = nil) #:nodoc: - unless perform_caching then block.call; return end - - buffer = yield - - if cache = read_fragment(name, options) - buffer.concat(cache) + if perform_caching + buffer = yield + + if cache = read_fragment(name, options) + buffer.concat(cache) + else + pos = buffer.length + block.call + write_fragment(name, buffer[pos..-1], options) + end else - pos = buffer.length block.call - write_fragment(name, buffer[pos..-1], options) end end diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index b40f1ba9be..7df987d525 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -125,7 +125,7 @@ module ActionController def call(env) @request = RackRequest.new(env) - @response = RackResponse.new + @response = RackResponse.new(@request) dispatch end @@ -134,14 +134,14 @@ module ActionController run_callbacks :prepare_dispatch Routing::Routes.reload - ActionView::TemplateFinder.reload! unless ActionView::Base.cache_template_loading + ActionController::Base.view_paths.reload! end # Cleanup the application by clearing out loaded classes so they can # be reloaded on the next request without restarting the server. def cleanup_application ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) - Dependencies.clear + ActiveSupport::Dependencies.clear ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) end diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index 12e1b4d493..2a732448f2 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -1,9 +1,10 @@ -require 'stringio' -require 'uri' - +require 'active_support/test_case' require 'action_controller/dispatcher' require 'action_controller/test_process' +require 'stringio' +require 'uri' + module ActionController module Integration #:nodoc: # An integration Session instance represents a set of requests and responses @@ -580,7 +581,7 @@ EOF # end # end # end - class IntegrationTest < Test::Unit::TestCase + class IntegrationTest < ActiveSupport::TestCase include Integration::Runner # Work around a bug in test/unit caused by the default test being named diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb index b5b59f2d7c..0721f71498 100644 --- a/actionpack/lib/action_controller/layout.rb +++ b/actionpack/lib/action_controller/layout.rb @@ -304,7 +304,7 @@ module ActionController #:nodoc: end def layout_directory?(layout_name) - @template.finder.find_template_extension_from_handler(File.join('layouts', layout_name)) + @template.view_paths.find_template_file_for_path("#{File.join('layouts', layout_name)}.#{@template.template_format}.erb") ? true : false end end end diff --git a/actionpack/lib/action_controller/performance_test.rb b/actionpack/lib/action_controller/performance_test.rb new file mode 100644 index 0000000000..85543fffae --- /dev/null +++ b/actionpack/lib/action_controller/performance_test.rb @@ -0,0 +1,16 @@ +require 'action_controller/integration' +require 'active_support/testing/performance' +require 'active_support/testing/default' + +module ActionController + # An integration test that runs a code profiler on your test methods. + # Profiling output for combinations of each test method, measurement, and + # output format are written to your tmp/performance directory. + # + # By default, process_time is measured and both flat and graph_html output + # formats are written, so you'll have two output files per test method. + class PerformanceTest < ActionController::IntegrationTest + include ActiveSupport::Testing::Performance + include ActiveSupport::Testing::Default + end +end diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb index 509fa6a08e..7c30bf0778 100644 --- a/actionpack/lib/action_controller/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -48,6 +48,9 @@ module ActionController # # # calls post_url(post) # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" + # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" + # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" # # ==== Options # @@ -83,8 +86,6 @@ module ActionController else [ record_or_hash_or_array ] end - args << format if format - inflection = case when options[:action].to_s == "new" @@ -96,6 +97,9 @@ module ActionController else :singular end + + args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} + args << format if format named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) send!(named_route, *args) @@ -136,11 +140,19 @@ module ActionController else record = records.pop route = records.inject("") do |string, parent| - string << "#{RecordIdentifier.send!("singular_class_name", parent)}_" + if parent.is_a?(Symbol) || parent.is_a?(String) + string << "#{parent}_" + else + string << "#{RecordIdentifier.send!("singular_class_name", parent)}_" + end end end - route << "#{RecordIdentifier.send!("#{inflection}_class_name", record)}_" + if record.is_a?(Symbol) || record.is_a?(String) + route << "#{record}_" + else + route << "#{RecordIdentifier.send!("#{inflection}_class_name", record)}_" + end action_prefix(options) + namespace + route + routing_type(options).to_s end @@ -163,16 +175,17 @@ module ActionController end end + # Remove the first symbols from the array and return the url prefix + # implied by those symbols. def extract_namespace(record_or_hash_or_array) - returning "" do |namespace| - if record_or_hash_or_array.is_a?(Array) - record_or_hash_or_array.delete_if do |record_or_namespace| - if record_or_namespace.is_a?(String) || record_or_namespace.is_a?(Symbol) - namespace << "#{record_or_namespace}_" - end - end - end + return "" unless record_or_hash_or_array.is_a?(Array) + + namespace_keys = [] + while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) + namespace_keys << record_or_hash_or_array.shift end + + namespace_keys.map {|k| "#{k}_"}.join end end end diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb index f42212e740..58a0a9280d 100644 --- a/actionpack/lib/action_controller/rack_process.rb +++ b/actionpack/lib/action_controller/rack_process.rb @@ -4,6 +4,7 @@ require 'action_controller/session/cookie_store' module ActionController #:nodoc: class RackRequest < AbstractRequest #:nodoc: attr_accessor :env, :session_options + attr_reader :cgi class SessionFixationAttempt < StandardError #:nodoc: end @@ -48,21 +49,12 @@ module ActionController #:nodoc: def cookies return {} unless @env["HTTP_COOKIE"] - if @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"] - @env["rack.request.cookie_hash"] - else + unless @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"] @env["rack.request.cookie_string"] = @env["HTTP_COOKIE"] - # According to RFC 2109: - # If multiple cookies satisfy the criteria above, they are ordered in - # the Cookie header such that those with more specific Path attributes - # precede those with less specific. Ordering with respect to other - # attributes (e.g., Domain) is unspecified. - @env["rack.request.cookie_hash"] = - parse_query(@env["rack.request.cookie_string"], ';,').inject({}) { |h, (k,v)| - h[k] = Array === v ? v.first : v - h - } + @env["rack.request.cookie_hash"] = CGI::Cookie::parse(@env["rack.request.cookie_string"]) end + + @env["rack.request.cookie_hash"] end def host_with_port_without_standard_port_handling @@ -169,37 +161,13 @@ end_msg def session_options_with_string_keys @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys end - - # From Rack::Utils - def parse_query(qs, d = '&;') - params = {} - (qs || '').split(/[#{d}] */n).inject(params) { |h,p| - k, v = unescape(p).split('=',2) - if cur = params[k] - if cur.class == Array - params[k] << v - else - params[k] = [cur, v] - end - else - params[k] = v - end - } - - return params - end - - def unescape(s) - s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){ - [$1.delete('%')].pack('H*') - } - end end class RackResponse < AbstractResponse #:nodoc: attr_accessor :status - def initialize + def initialize(request) + @request = request @writer = lambda { |x| @body << x } @block = nil super() @@ -221,6 +189,8 @@ end_msg if @body.respond_to?(:call) @writer = lambda { |x| callback.call(x) } @body.call(self, self) + elsif @body.is_a?(String) + @body.each_line(&callback) else @body.each(&callback) end @@ -270,9 +240,9 @@ end_msg else cookies << cookie.to_s end - @output_cookies.each { |c| cookies << c.to_s } if @output_cookies + @request.cgi.output_cookies.each { |c| cookies << c.to_s } if @request.cgi.output_cookies - headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].compact.join("\n") + headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact end options.each { |k,v| headers[k] = v } @@ -283,6 +253,8 @@ end_msg end class CGIWrapper < ::CGI + attr_reader :output_cookies + def initialize(request, *args) @request = request @args = *args diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 643ff7e5f4..742d290ad6 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -31,18 +31,21 @@ module ActionController module RecordIdentifier extend self + JOIN = '_'.freeze + NEW = 'new'.freeze + # Returns plural/singular for a record or class. Example: # # partial_path(post) # => "posts/post" # partial_path(Person) # => "people/person" # partial_path(Person, "admin/games") # => "admin/people/person" def partial_path(record_or_class, controller_path = nil) - klass = class_from_record_or_class(record_or_class) + name = model_name_from_record_or_class(record_or_class) if controller_path && controller_path.include?("/") - "#{File.dirname(controller_path)}/#{klass.name.tableize}/#{klass.name.demodulize.underscore}" + "#{File.dirname(controller_path)}/#{name.partial_path}" else - "#{klass.name.tableize}/#{klass.name.demodulize.underscore}" + name.partial_path end end @@ -56,21 +59,25 @@ module ActionController # dom_class(post, :edit) # => "edit_post" # dom_class(Person, :edit) # => "edit_person" def dom_class(record_or_class, prefix = nil) - [ prefix, singular_class_name(record_or_class) ].compact * '_' + singular = singular_class_name(record_or_class) + prefix ? "#{prefix}#{JOIN}#{singular}" : singular end # The DOM id convention is to use the singular form of an object or class with the id following an underscore. # If no id is found, prefix with "new_" instead. Examples: # - # dom_id(Post.new(:id => 45)) # => "post_45" + # dom_id(Post.find(45)) # => "post_45" # dom_id(Post.new) # => "new_post" # # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id: # - # dom_id(Post.new(:id => 45), :edit) # => "edit_post_45" + # dom_id(Post.find(45), :edit) # => "edit_post_45" def dom_id(record, prefix = nil) - prefix ||= 'new' unless record.id - [ prefix, singular_class_name(record), record.id ].compact * '_' + if record_id = record.id + "#{dom_class(record, prefix)}#{JOIN}#{record_id}" + else + dom_class(record, prefix || NEW) + end end # Returns the plural class name of a record or class. Examples: @@ -78,7 +85,7 @@ module ActionController # plural_class_name(post) # => "posts" # plural_class_name(Highrise::Person) # => "highrise_people" def plural_class_name(record_or_class) - singular_class_name(record_or_class).pluralize + model_name_from_record_or_class(record_or_class).plural end # Returns the singular class name of a record or class. Examples: @@ -86,12 +93,12 @@ module ActionController # singular_class_name(post) # => "post" # singular_class_name(Highrise::Person) # => "highrise_person" def singular_class_name(record_or_class) - class_from_record_or_class(record_or_class).name.underscore.tr('/', '_') + model_name_from_record_or_class(record_or_class).singular end private - def class_from_record_or_class(record_or_class) - record_or_class.is_a?(Class) ? record_or_class : record_or_class.class + def model_name_from_record_or_class(record_or_class) + (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 914163a709..38f8e10fe7 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -134,14 +134,15 @@ module ActionController # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- # delimited list in the case of multiple chained proxies; the last # address which is not trusted is the originating IP. - def remote_ip if TRUSTED_PROXIES !~ @env['REMOTE_ADDR'] return @env['REMOTE_ADDR'] end + remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',') + if @env.include? 'HTTP_CLIENT_IP' - if @env.include? 'HTTP_X_FORWARDED_FOR' + if remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP']) # We don't know which came from the proxy, and which from the user raise ActionControllerError.new(<<EOM) IP spoofing attack?! @@ -149,11 +150,11 @@ HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} EOM end + return @env['HTTP_CLIENT_IP'] end - if @env.include? 'HTTP_X_FORWARDED_FOR' then - remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',') + if remote_ips while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip remote_ips.pop end diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb index 79ec2b13b7..b11aa5625b 100644 --- a/actionpack/lib/action_controller/resources.rb +++ b/actionpack/lib/action_controller/resources.rb @@ -72,7 +72,7 @@ module ActionController end def conditions - @conditions = @options[:conditions] || {} + @conditions ||= @options[:conditions] || {} end def path @@ -80,9 +80,9 @@ module ActionController end def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] + new_action = self.options[:path_names][:new] if self.options[:path_names] new_action ||= Base.resources_path_names[:new] - @new_path ||= "#{path}/#{new_action}" + @new_path ||= "#{path}/#{new_action}" end def member_path diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index 6aa266513d..8846dcc504 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -369,7 +369,7 @@ module ActionController Routes = RouteSet.new - ::Inflector.module_eval do + ActiveSupport::Inflector.module_eval do # Ensures that routes are reloaded when Rails inflections are updated. def inflections_with_route_reloading(&block) returning(inflections_without_route_reloading(&block)) { diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 864e068004..f0ad066bad 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -249,7 +249,7 @@ module ActionController end def extract_value - "#{local_name} = hash[:#{key}] && hash[:#{key}].collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" + "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" end def default diff --git a/actionpack/lib/action_controller/templates/rescues/layout.erb b/actionpack/lib/action_controller/templates/rescues/layout.erb index d38f3e67f9..4a04742e40 100644 --- a/actionpack/lib/action_controller/templates/rescues/layout.erb +++ b/actionpack/lib/action_controller/templates/rescues/layout.erb @@ -1,4 +1,4 @@ -<html> +<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Action Controller: Exception caught</title> <style> diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb index 607fd186b9..b8d73c350d 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb @@ -17,7 +17,7 @@ module HTML #:nodoc: @root = Node.new(nil) node_stack = [ @root ] while token = tokenizer.next - node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token) + node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict) node_stack.last.children << node unless node.tag? && node.closing == :close if node.tag? diff --git a/actionpack/lib/action_controller/verification.rb b/actionpack/lib/action_controller/verification.rb index 9f606e7b7c..35b12a7f13 100644 --- a/actionpack/lib/action_controller/verification.rb +++ b/actionpack/lib/action_controller/verification.rb @@ -116,7 +116,7 @@ module ActionController #:nodoc: end def apply_redirect_to(redirect_to_option) # :nodoc: - redirect_to_option.is_a?(Symbol) ? self.send!(redirect_to_option) : redirect_to_option + (redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.send!(redirect_to_option) : redirect_to_option end def apply_remaining_actions(options) # :nodoc: diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 5f4126e4e9..973020a768 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -21,13 +21,9 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -require 'action_view/template_handler' -require 'action_view/template_handlers/compilable' -require 'action_view/template_handlers/builder' -require 'action_view/template_handlers/erb' -require 'action_view/template_handlers/rjs' - -require 'action_view/template_finder' +require 'action_view/template_handlers' +require 'action_view/template_file' +require 'action_view/view_load_paths' require 'action_view/template' require 'action_view/partial_template' require 'action_view/inline_template' diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index c236666dcd..91a86470d3 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -1,17 +1,17 @@ module ActionView #:nodoc: class ActionViewError < StandardError #:nodoc: end - + class MissingTemplate < ActionViewError #:nodoc: end - # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb - # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. + # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb + # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. # If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. - # + # # = ERb - # - # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # + # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the # following loop for names: # # <b>Names of all the people</b> @@ -51,7 +51,7 @@ module ActionView #:nodoc: # <title><%= @page_title %></title> # # == Passing local variables to sub templates - # + # # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: # # <%= render "shared/header", { :headline => "Welcome", :person => person } %> @@ -77,8 +77,8 @@ module ActionView #:nodoc: # # == Builder # - # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object - # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. + # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object + # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. # # Here are some basic examples: # @@ -87,7 +87,7 @@ module ActionView #:nodoc: # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> # xml.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\> # # NOTE: order of attributes is not specified. - # + # # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: # # xml.div { @@ -111,7 +111,7 @@ module ActionView #:nodoc: # xml.description "Basecamp: Recent items" # xml.language "en-us" # xml.ttl "40" - # + # # for item in @recent_items # xml.item do # xml.title(item_title(item)) @@ -119,7 +119,7 @@ module ActionView #:nodoc: # xml.pubDate(item_pubDate(item)) # xml.guid(@person.firm.account.url + @recent_items.url(item)) # xml.link(@person.firm.account.url + @recent_items.url(item)) - # + # # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) # end # end @@ -130,12 +130,12 @@ module ActionView #:nodoc: # # == JavaScriptGenerator # - # JavaScriptGenerator templates end in <tt>.rjs</tt>. Unlike conventional templates which are used to - # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to - # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax + # JavaScriptGenerator templates end in <tt>.rjs</tt>. Unlike conventional templates which are used to + # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to + # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax # and make updates to the page where the request originated from. - # - # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block. + # + # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block. # # When an <tt>.rjs</tt> action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example: # @@ -145,48 +145,45 @@ module ActionView #:nodoc: # # page.replace_html 'sidebar', :partial => 'sidebar' # page.remove "person-#{@person.id}" - # page.visual_effect :highlight, 'user-list' + # page.visual_effect :highlight, 'user-list' # # This refreshes the sidebar, removes a person element and highlights the user list. - # + # # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details. class Base include ERB::Util - attr_reader :finder attr_accessor :base_path, :assigns, :template_extension, :first_render attr_accessor :controller - + attr_writer :template_format attr_accessor :current_render_extension - # Specify trim mode for the ERB compiler. Defaults to '-'. - # See ERb documentation for suitable values. - @@erb_trim_mode = '-' - cattr_accessor :erb_trim_mode + attr_accessor :output_buffer + + class << self + delegate :erb_trim_mode=, :to => 'ActionView::TemplateHandlers::ERB' + end # Specify whether file modification times should be checked to see if a template needs recompilation @@cache_template_loading = false cattr_accessor :cache_template_loading - + def self.cache_template_extensions=(*args) ActiveSupport::Deprecation.warn("config.action_view.cache_template_extensions option has been deprecated and has no effect. " << "Please remove it from your config files.", caller) end # Specify whether RJS responses should be wrapped in a try/catch block - # that alert()s the caught exception (and then re-raises it). + # that alert()s the caught exception (and then re-raises it). @@debug_rjs = false cattr_accessor :debug_rjs - - @@erb_variable = '_erbout' - cattr_accessor :erb_variable - + attr_internal :request delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, - :flash, :logger, :action_name, :to => :controller - + :flash, :logger, :action_name, :controller_name, :to => :controller + module CompiledTemplates #:nodoc: # holds compiled template code end @@ -222,11 +219,17 @@ module ActionView #:nodoc: @assigns = assigns_for_first_render @assigns_added = nil @controller = controller - @finder = TemplateFinder.new(self, view_paths) + self.view_paths = view_paths + end + + attr_reader :view_paths + + def view_paths=(paths) + @view_paths = ViewLoadPaths.new(Array(paths)) end - # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true, - # it's relative to the view_paths array, otherwise it's absolute. The hash in <tt>local_assigns</tt> + # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true, + # it's relative to the view_paths array, otherwise it's absolute. The hash in <tt>local_assigns</tt> # is made available as local variables. def render_file(template_path, use_full_path = true, local_assigns = {}) #:nodoc: if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/") @@ -241,11 +244,11 @@ If you are rendering a subtemplate, you must now use controller-like partial syn render :partial => 'signup' # no mailer_name necessary END_ERROR end - + Template.new(self, template_path, use_full_path, local_assigns).render_template end - - # Renders the template present at <tt>template_path</tt> (relative to the view_paths array). + + # Renders the template present at <tt>template_path</tt> (relative to the view_paths array). # The hash in <tt>local_assigns</tt> is made available as local variables. def render(options = {}, local_assigns = {}, &block) #:nodoc: if options.is_a?(String) @@ -253,12 +256,13 @@ If you are rendering a subtemplate, you must now use controller-like partial syn elsif options == :update update_page(&block) elsif options.is_a?(Hash) + use_full_path = options[:use_full_path] options = options.reverse_merge(:locals => {}, :use_full_path => true) if partial_layout = options.delete(:layout) if block_given? - wrap_content_for_layout capture(&block) do - concat(render(options.merge(:partial => partial_layout)), block.binding) + wrap_content_for_layout capture(&block) do + concat(render(options.merge(:partial => partial_layout))) end else wrap_content_for_layout render(options) do @@ -266,7 +270,7 @@ If you are rendering a subtemplate, you must now use controller-like partial syn end end elsif options[:file] - render_file(options[:file], options[:use_full_path], options[:locals]) + render_file(options[:file], use_full_path || false, options[:locals]) elsif options[:partial] && options[:collection] render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals]) elsif options[:partial] @@ -314,11 +318,16 @@ If you are rendering a subtemplate, you must now use controller-like partial syn end end + def file_exists?(template_path) + view_paths.template_exists?(template_file_from_name(template_path)) + end + private def wrap_content_for_layout(content) - original_content_for_layout = @content_for_layout - @content_for_layout = content - returning(yield) { @content_for_layout = original_content_for_layout } + original_content_for_layout, @content_for_layout = @content_for_layout, content + yield + ensure + @content_for_layout = original_content_for_layout end # Evaluate the local assigns and pushes them to the view. @@ -333,11 +342,43 @@ If you are rendering a subtemplate, you must now use controller-like partial syn def assign_variables_from_controller @assigns.each { |key, value| instance_variable_set("@#{key}", value) } end - + def execute(template) send(template.method, template.locals) do |*names| instance_variable_get "@content_for_#{names.first || 'layout'}" - end + end + end + + def template_file_from_name(template_name) + template_name = TemplateFile.from_path(template_name) + pick_template_extension(template_name) unless template_name.extension + end + + # Gets the extension for an existing template with the given template_path. + # Returns the format with the extension if that template exists. + # + # pick_template_extension('users/show') + # # => 'html.erb' + # + # pick_template_extension('users/legacy') + # # => "rhtml" + # + def pick_template_extension(file) + if f = self.view_paths.find_template_file_for_path(file.dup_with_extension(template_format)) || file_from_first_render(file) + f + elsif template_format == :js && f = self.view_paths.find_template_file_for_path(file.dup_with_extension(:html)) + @template_format = :html + f + else + nil + end + end + + # Determine the template extension from the <tt>@first_render</tt> filename + def file_from_first_render(file) + if extension = File.basename(@first_render.to_s)[/^[^.]+\.(.+)$/, 1] + file.dup_with_extension(extension) + end end end end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 9ea06568cf..990c30b90d 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -31,20 +31,16 @@ module ActionView # </body></html> # def capture(*args, &block) - # execute the block - begin - buffer = eval(ActionView::Base.erb_variable, block.binding) - rescue - buffer = nil - end - - if buffer.nil? - capture_block(*args, &block).to_s + # Return captured buffer in erb. + if block_called_from_erb?(block) + with_output_buffer { block.call(*args) } + + # Return block result otherwise, but protect buffer also. else - capture_erb_with_buffer(buffer, *args, &block).to_s + with_output_buffer { return block.call(*args) } end end - + # Calling content_for stores a block of markup in an identifier for later use. # You can make subsequent calls to the stored content in other templates or the layout # by passing the identifier as an argument to <tt>yield</tt>. @@ -121,40 +117,19 @@ module ActionView # named <tt>@content_for_#{name_of_the_content_block}</tt>. The preferred usage is now # <tt><%= yield :footer %></tt>. def content_for(name, content = nil, &block) - existing_content_for = instance_variable_get("@content_for_#{name}").to_s - new_content_for = existing_content_for + (block_given? ? capture(&block) : content) - instance_variable_set("@content_for_#{name}", new_content_for) + ivar = "@content_for_#{name}" + content = capture(&block) if block_given? + instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}") + nil end private - def capture_block(*args, &block) - block.call(*args) - end - - def capture_erb(*args, &block) - buffer = eval(ActionView::Base.erb_variable, block.binding) - capture_erb_with_buffer(buffer, *args, &block) - end - - def capture_erb_with_buffer(buffer, *args, &block) - pos = buffer.length - block.call(*args) - - # extract the block - data = buffer[pos..-1] - - # replace it in the original with empty string - buffer[pos..-1] = '' - - data - end - - def erb_content_for(name, &block) - eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)" - end - - def block_content_for(name, &block) - eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)" + def with_output_buffer(buf = '') + self.output_buffer, old_buffer = buf, output_buffer + yield + output_buffer + ensure + self.output_buffer = old_buffer end end end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index af43397cb7..f952ada691 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -249,9 +249,9 @@ module ActionView args.unshift object end - concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) + concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {})) fields_for(object_name, *(args << options), &proc) - concat('</form>', proc.binding) + concat('</form>') end def apply_form_for_options!(object_or_array, options) #:nodoc: diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index c0cba24be4..f22c12c9ad 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -306,7 +306,7 @@ module ActionView # # NOTE: Only the option tags are returned, you have to wrap this call in # a regular HTML select tag. - def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) + def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone) zone_options = "" zones = model.all @@ -419,7 +419,7 @@ module ActionView value = value(object) content_tag("select", add_options( - time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || TimeZone), + time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), options, value ), html_options ) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index ca58f4ba26..ccebec3692 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -6,7 +6,7 @@ module ActionView # Provides a number of methods for creating form tags that doesn't rely on an Active Record object assigned to the template like # FormHelper does. Instead, you provide the names and values manually. # - # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying + # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying # <tt>:disabled => true</tt> will give <tt>disabled="disabled"</tt>. module FormTagHelper # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like @@ -20,15 +20,15 @@ module ActionView # * A list of parameters to feed to the URL the form will be posted to. # # ==== Examples - # form_tag('/posts') + # form_tag('/posts') # # => <form action="/posts" method="post"> # - # form_tag('/posts/1', :method => :put) + # form_tag('/posts/1', :method => :put) # # => <form action="/posts/1" method="put"> # - # form_tag('/upload', :multipart => true) + # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> - # + # # <% form_tag '/posts' do -%> # <div><%= submit_tag 'Save' %></div> # <% end -%> @@ -88,7 +88,7 @@ module ActionView # * <tt>:size</tt> - The number of visible characters that will fit in the input. # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. # * Any other key creates standard HTML attributes for the tag. - # + # # ==== Examples # text_field_tag 'name' # # => <input id="name" name="name" type="text" /> @@ -146,13 +146,13 @@ module ActionView # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" /> # # hidden_field_tag 'collected_input', '', :onchange => "alert('Input collected!')" - # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')" + # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')" # # type="hidden" value="" /> def hidden_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) end - # Creates a file upload field. If you are using file uploads then you will also need + # Creates a file upload field. If you are using file uploads then you will also need # to set the multipart option for the form tag: # # <%= form_tag { :action => "post" }, { :multipart => true } %> @@ -160,7 +160,7 @@ module ActionView # <%= submit_tag %> # <%= end_form_tag %> # - # The specified URL will then be passed a File object containing the selected file, or if the field + # The specified URL will then be passed a File object containing the selected file, or if the field # was left blank, a StringIO object. # # ==== Options @@ -181,7 +181,7 @@ module ActionView # # => <input id="resume" name="resume" type="file" value="~/resume.doc" /> # # file_field_tag 'user_pic', :accept => 'image/png,image/gif,image/jpeg' - # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" /> + # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" /> # # file_field_tag 'file', :accept => 'text/html', :class => 'upload', :value => 'index.html' # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" /> @@ -286,7 +286,7 @@ module ActionView tag :input, html_options end - # Creates a radio button; use groups of radio buttons named the same to allow users to + # Creates a radio button; use groups of radio buttons named the same to allow users to # select from a group of options. # # ==== Options @@ -313,14 +313,14 @@ module ActionView tag :input, html_options end - # Creates a submit button with the text <tt>value</tt> as the caption. + # Creates a submit button with the text <tt>value</tt> as the caption. # # ==== Options # * <tt>:confirm => 'question?'</tt> - This will add a JavaScript confirm # prompt with the question specified. If the user accepts, the form is # processed normally, otherwise no action is taken. # * <tt>:disabled</tt> - If true, the user will not be able to use this input. - # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a disabled version + # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a disabled version # of the submit button when the form is submitted. # * Any other key creates standard HTML options for the tag. # @@ -335,7 +335,7 @@ module ActionView # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" /> # # submit_tag "Complete sale", :disable_with => "Please wait..." - # # => <input name="commit" onclick="this.disabled=true;this.value='Please wait...';this.form.submit();" + # # => <input name="commit" onclick="this.disabled=true;this.value='Please wait...';this.form.submit();" # # type="submit" value="Complete sale" /> # # submit_tag nil, :class => "form_submit" @@ -346,7 +346,7 @@ module ActionView # # name="commit" type="submit" value="Edit" /> def submit_tag(value = "Save changes", options = {}) options.stringify_keys! - + if disable_with = options.delete("disable_with") options["onclick"] = [ "this.setAttribute('originalValue', this.value)", @@ -358,15 +358,15 @@ module ActionView "return result;", ].join(";") end - + if confirm = options.delete("confirm") options["onclick"] ||= '' options["onclick"] += "return #{confirm_javascript_function(confirm)};" end - + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) end - + # Displays an image which when clicked will submit the form. # # <tt>source</tt> is passed to AssetTagHelper#image_path @@ -407,12 +407,12 @@ module ActionView # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, &block) content = capture(&block) - concat(tag(:fieldset, {}, true), block.binding) - concat(content_tag(:legend, legend), block.binding) unless legend.blank? - concat(content, block.binding) - concat("</fieldset>", block.binding) + concat(tag(:fieldset, {}, true)) + concat(content_tag(:legend, legend)) unless legend.blank? + concat(content) + concat("</fieldset>") end - + private def html_options_for_form(url_for_options, options, *parameters_for_url) returning options.stringify_keys do |html_options| @@ -420,7 +420,7 @@ module ActionView html_options["action"] = url_for(url_for_options, *parameters_for_url) end end - + def extra_tags_for_form(html_options) case method = html_options.delete("method").to_s when /^get$/i # must be case-insentive, but can't use downcase as might be nil @@ -434,17 +434,17 @@ module ActionView content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0') end end - + def form_tag_html(html_options) extra_tags = extra_tags_for_form(html_options) tag(:form, html_options, true) + extra_tags end - + def form_tag_in_block(html_options, &block) content = capture(&block) - concat(form_tag_html(html_options), block.binding) - concat(content, block.binding) - concat("</form>", block.binding) + concat(form_tag_html(html_options)) + concat(content) + concat("</form>") end def token_tag diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index ed931e064f..00092adc87 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -4,10 +4,10 @@ require 'action_view/helpers/prototype_helper' module ActionView module Helpers # Provides functionality for working with JavaScript in your views. - # + # # == Ajax, controls and visual effects - # - # * For information on using Ajax, see + # + # * For information on using Ajax, see # ActionView::Helpers::PrototypeHelper. # * For information on using controls and visual effects, see # ActionView::Helpers::ScriptaculousHelper. @@ -20,22 +20,22 @@ module ActionView # and ActionView::Helpers::ScriptaculousHelper), you must do one of the # following: # - # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD - # section of your page (recommended): This function will return + # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD + # section of your page (recommended): This function will return # references to the JavaScript files created by the +rails+ command in # your <tt>public/javascripts</tt> directory. Using it is recommended as - # the browser can then cache the libraries instead of fetching all the + # the browser can then cache the libraries instead of fetching all the # functions anew on every request. - # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but + # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but # will only include the Prototype core library, which means you are able - # to use all basic AJAX functionality. For the Scriptaculous-based - # JavaScript helpers, like visual effects, autocompletion, drag and drop + # to use all basic AJAX functionality. For the Scriptaculous-based + # JavaScript helpers, like visual effects, autocompletion, drag and drop # and so on, you should use the method described above. # * Use <tt><%= define_javascript_functions %></tt>: this will copy all the # JavaScript support functions within a single script block. Not # recommended. # - # For documentation on +javascript_include_tag+ see + # For documentation on +javascript_include_tag+ see # ActionView::Helpers::AssetTagHelper. module JavaScriptHelper unless const_defined? :JAVASCRIPT_PATH @@ -53,7 +53,7 @@ module ActionView # # The +function+ argument can be omitted in favor of an +update_page+ # block, which evaluates to a string when the template is rendered - # (instead of making an Ajax request first). + # (instead of making an Ajax request first). # # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" # @@ -79,28 +79,23 @@ module ActionView # <a href="#" id="more_link" onclick="try { # $("details").visualEffect("toggle_blind"); # $("more_link").update("Show me less"); - # } - # catch (e) { - # alert('RJS error:\n\n' + e.toString()); + # } + # catch (e) { + # alert('RJS error:\n\n' + e.toString()); # alert('$(\"details\").visualEffect(\"toggle_blind\"); # \n$(\"more_link\").update(\"Show me less\");'); - # throw e + # throw e # }; # return false;">Show me more</a> # def link_to_function(name, *args, &block) - html_options = args.extract_options! - function = args[0] || '' - - html_options.symbolize_keys! - function = update_page(&block) if block_given? - content_tag( - "a", name, - html_options.merge({ - :href => html_options[:href] || "#", - :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;" - }) - ) + html_options = args.extract_options!.symbolize_keys + + function = block_given? ? update_page(&block) : args[0] || '' + onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" + href = html_options[:href] || '#' + + content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick)) end # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the @@ -112,7 +107,7 @@ module ActionView # # The +function+ argument can be omitted in favor of an +update_page+ # block, which evaluates to a string when the template is rendered - # (instead of making an Ajax request first). + # (instead of making an Ajax request first). # # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" # @@ -128,45 +123,56 @@ module ActionView # page[:details].visual_effect :toggle_slide # end def button_to_function(name, *args, &block) - html_options = args.extract_options! - function = args[0] || '' - - html_options.symbolize_keys! - function = update_page(&block) if block_given? - tag(:input, html_options.merge({ - :type => "button", :value => name, - :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" - })) + html_options = args.extract_options!.symbolize_keys + + function = block_given? ? update_page(&block) : args[0] || '' + onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" + + tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) end - # Includes the Action Pack JavaScript libraries inside a single <script> + # Includes the Action Pack JavaScript libraries inside a single <script> # tag. The function first includes prototype.js and then its core extensions, # (determined by filenames starting with "prototype"). # Afterwards, any additional scripts will be included in undefined order. # # Note: The recommended approach is to copy the contents of # lib/action_view/helpers/javascripts/ into your application's - # public/javascripts/ directory, and use +javascript_include_tag+ to + # public/javascripts/ directory, and use +javascript_include_tag+ to # create remote <script> links. def define_javascript_functions javascript = "<script type=\"#{Mime::JS}\">" - - # load prototype.js and its extensions first + + # load prototype.js and its extensions first prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse - prototype_libs.each do |filename| + prototype_libs.each do |filename| javascript << "\n" << IO.read(filename) end - + # load other libraries - (Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename| + (Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename| javascript << "\n" << IO.read(filename) end javascript << '</script>' end + + JS_ESCAPE_MAP = { + '\\' => '\\\\', + '</' => '<\/', + "\r\n" => '\n', + "\n" => '\n', + "\r" => '\n', + '"' => '\\"', + "'" => "\\'" } + # Escape carrier returns and single and double quotes for JavaScript segments. def escape_javascript(javascript) - (javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } + if javascript + javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] } + else + '' + end end # Returns a JavaScript tag with the +content+ inside. Example: @@ -180,7 +186,7 @@ module ActionView # </script> # # +html_options+ may be a hash of attributes for the <script> tag. Example: - # javascript_tag "alert('All is good')", :defer => 'defer' + # javascript_tag "alert('All is good')", :defer => 'defer' # # => <script defer="defer" type="text/javascript">alert('All is good')</script> # # Instead of passing the content as an argument, you can also use a block @@ -189,46 +195,45 @@ module ActionView # alert('All is good') # <% end -%> def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) - if block_given? - html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) - content = capture(&block) - else - content = content_or_options_with_block - end + content = + if block_given? + html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + capture(&block) + else + content_or_options_with_block + end + + tag = content_tag(:script, javascript_cdata_section(content), html_options.merge(:type => Mime::JS)) - javascript_tag = content_tag("script", javascript_cdata_section(content), html_options.merge(:type => Mime::JS)) - - if block_given? && block_is_within_action_view?(block) - concat(javascript_tag, block.binding) + if block_called_from_erb?(block) + concat(tag) else - javascript_tag + tag end end def javascript_cdata_section(content) #:nodoc: "\n//#{cdata_section("\n#{content}\n//")}\n" end - + protected def options_for_javascript(options) - '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}' + if options.empty? + '{}' + else + "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}" + end end - + def array_or_string_for_javascript(option) - js_option = if option.kind_of?(Array) + if option.kind_of?(Array) "['#{option.join('\',\'')}']" elsif !option.nil? "'#{option}'" end - js_option - end - - private - def block_is_within_action_view?(block) - eval("defined? _erbout", block.binding) end end - + JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper end end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index c731824f18..9f6e550c09 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -382,9 +382,9 @@ module ActionView args.unshift object end - concat(form_remote_tag(options), proc.binding) + concat(form_remote_tag(options)) fields_for(object_name, *(args << options), &proc) - concat('</form>', proc.binding) + concat('</form>') end alias_method :form_remote_for, :remote_form_for @@ -868,6 +868,16 @@ module ActionView record "window.location.href = #{url.inspect}" end + # Reloads the browser's current +location+ using JavaScript + # + # Examples: + # + # # Generates: window.location.reload(); + # page.reload + def reload + record 'window.location.reload()' + end + # Calls the JavaScript +function+, optionally with the given +arguments+. # # If a block is given, the block will be passed to a new JavaScriptGenerator; diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 66c596f3a9..9bb235175e 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -51,9 +51,8 @@ module ActionView prefix = args.first.is_a?(Hash) ? nil : args.shift options = args.first.is_a?(Hash) ? args.shift : {} concat content_tag(tag_name, capture(&block), - options.merge({ :class => "#{dom_class(record)} #{options[:class]}".strip, :id => dom_id(record, prefix) })), - block.binding + options.merge({ :class => "#{dom_class(record)} #{options[:class]}".strip, :id => dom_id(record, prefix) })) end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 999cbfb52a..aeafd3906d 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -1,5 +1,6 @@ require 'cgi' require 'erb' +require 'set' module ActionView module Helpers #:nodoc: @@ -8,16 +9,17 @@ module ActionView module TagHelper include ERB::Util - BOOLEAN_ATTRIBUTES = Set.new(%w(disabled readonly multiple)) + BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple).to_set + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) - # Returns an empty HTML tag of type +name+ which by default is XHTML - # compliant. Set +open+ to true to create an open tag compatible - # with HTML 4.0 and below. Add HTML attributes by passing an attributes + # Returns an empty HTML tag of type +name+ which by default is XHTML + # compliant. Set +open+ to true to create an open tag compatible + # with HTML 4.0 and below. Add HTML attributes by passing an attributes # hash to +options+. Set +escape+ to false to disable attribute value # escaping. # # ==== Options - # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use # symbols or strings for the attribute names. # @@ -28,7 +30,7 @@ module ActionView # tag("br", nil, true) # # => <br> # - # tag("input", { :type => 'text', :disabled => true }) + # tag("input", { :type => 'text', :disabled => true }) # # => <input type="text" disabled="disabled" /> # # tag("img", { :src => "open & shut.png" }) @@ -37,17 +39,17 @@ module ActionView # tag("img", { :src => "open & shut.png" }, false, false) # # => <img src="open & shut.png" /> def tag(name, options = nil, open = false, escape = true) - "<#{name}#{tag_options(options, escape) if options}" + (open ? ">" : " />") + "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}" end # Returns an HTML block tag of type +name+ surrounding the +content+. Add - # HTML attributes by passing an attributes hash to +options+. + # HTML attributes by passing an attributes hash to +options+. # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +options+ as the second parameter. # Set escape to false to disable attribute value escaping. # # ==== Options - # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use # symbols or strings for the attribute names. # @@ -66,12 +68,15 @@ module ActionView def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) if block_given? options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) - content = capture(&block) - content_tag = content_tag_string(name, content, options, escape) - block_is_within_action_view?(block) ? concat(content_tag, block.binding) : content_tag + content_tag = content_tag_string(name, capture(&block), options, escape) + + if block_called_from_erb?(block) + concat(content_tag) + else + content_tag + end else - content = content_or_options_with_block - content_tag_string(name, content, options, escape) + content_tag_string(name, content_or_options_with_block, options, escape) end end @@ -103,6 +108,16 @@ module ActionView end private + BLOCK_CALLED_FROM_ERB = 'defined? __in_erb_template' + + # Check whether we're called from an erb template. + # We'd return a string in any other case, but erb <%= ... %> + # can't take an <% end %> later on, so we have to use <% ... %> + # and implicitly concat. + def block_called_from_erb?(block) + block && eval(BLOCK_CALLED_FROM_ERB, block) + end + def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options "<#{name}#{tag_options}>#{content}</#{name}>" @@ -114,7 +129,6 @@ module ActionView if escape options.each do |key, value| next unless value - key = key.to_s value = BOOLEAN_ATTRIBUTES.include?(key) ? key : escape_once(value) attrs << %(#{key}="#{value}") end @@ -124,10 +138,6 @@ module ActionView " #{attrs.sort * ' '}" unless attrs.empty? end end - - def block_is_within_action_view?(block) - eval("defined? _erbout", block.binding) - end end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 669a285424..d98c5bd0d5 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -15,18 +15,22 @@ module ActionView # # ==== Examples # <% - # concat "hello", binding + # concat "hello" # # is the equivalent of <%= "hello" %> # # if (logged_in == true): - # concat "Logged in!", binding + # concat "Logged in!" # else - # concat link_to('login', :action => login), binding + # concat link_to('login', :action => login) # end # # will either display "Logged in!" or a login link # %> - def concat(string, binding) - eval(ActionView::Base.erb_variable, binding) << string + def concat(string, unused_binding = nil) + if unused_binding + ActiveSupport::Deprecation.warn("The binding argument of #concat is no longer needed. Please remove it from your views and helpers.") + end + + output_buffer << string end if RUBY_VERSION < '1.9' diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 7bb189420b..89166fe19a 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -90,6 +90,13 @@ module ActionView # link will be used in place of a referrer if none exists. If nil is passed as # a name, the link itself will become the name. # + # ==== Signatures + # + # link_to(name, options = {}, html_options = nil) + # link_to(options = {}, html_options = nil) do + # # name + # end + # # ==== Options # * <tt>:confirm => 'question?'</tt> - This will add a JavaScript confirm # prompt with the question specified. If the user accepts, the link is @@ -147,6 +154,13 @@ module ActionView # link_to "Profiles", :controller => "profiles" # # => <a href="/profiles">Profiles</a> # + # You can use a block as well if your link target is hard to fit into the name parameter. ERb example: + # + # <% link_to(@profile) do %> + # <strong><%= @profile.name %></strong> -- <span>Check it out!!</span> + # <% end %> + # # => <a href="/profiles/1"><strong>David</strong> -- <span>Check it out!!</span></a> + # # Classes and ids for CSS are easy to produce: # # link_to "Articles", articles_path, :id => "news", :class => "article" @@ -189,27 +203,37 @@ module ActionView # f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; # var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); # m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;">Delete Image</a> - def link_to(name, options = {}, html_options = nil) - url = case options - when String - options - when :back - @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' + def link_to(*args, &block) + if block_given? + options = args.first || {} + html_options = args.second + concat(link_to(capture(&block), options, html_options)) + else + name = args.first + options = args.second || {} + html_options = args.third + + url = case options + when String + options + when :back + @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' + else + self.url_for(options) + end + + if html_options + html_options = html_options.stringify_keys + href = html_options['href'] + convert_options_to_javascript!(html_options, url) + tag_options = tag_options(html_options) else - self.url_for(options) + tag_options = nil end - - if html_options - html_options = html_options.stringify_keys - href = html_options['href'] - convert_options_to_javascript!(html_options, url) - tag_options = tag_options(html_options) - else - tag_options = nil + + href_attr = "href=\"#{url}\"" unless href + "<a #{href_attr}#{tag_options}>#{name || url}</a>" end - - href_attr = "href=\"#{url}\"" unless href - "<a #{href_attr}#{tag_options}>#{name || url}</a>" end # Generates a form containing a single button that submits to the URL created @@ -511,7 +535,7 @@ module ActionView when method "#{method_javascript_function(method, url, href)}return false;" when popup - popup_javascript_function(popup) + 'return false;' + "#{popup_javascript_function(popup)}return false;" else html_options["onclick"] end diff --git a/actionpack/lib/action_view/inline_template.rb b/actionpack/lib/action_view/inline_template.rb index 87c012d181..fd0ad48302 100644 --- a/actionpack/lib/action_view/inline_template.rb +++ b/actionpack/lib/action_view/inline_template.rb @@ -1,20 +1,17 @@ module ActionView #:nodoc: class InlineTemplate < Template #:nodoc: - def initialize(view, source, locals = {}, type = nil) @view = view - @finder = @view.finder - + @source = source @extension = type @locals = locals || {} - + @handler = self.class.handler_class_for_extension(@extension).new(@view) end - + def method_key @source end - end end diff --git a/actionpack/lib/action_view/partial_template.rb b/actionpack/lib/action_view/partial_template.rb index 1fb3aaee02..0cf996ca04 100644 --- a/actionpack/lib/action_view/partial_template.rb +++ b/actionpack/lib/action_view/partial_template.rb @@ -1,70 +1,70 @@ module ActionView #:nodoc: class PartialTemplate < Template #:nodoc: - attr_reader :variable_name, :object - + def initialize(view, partial_path, object = nil, locals = {}) - @path, @variable_name = extract_partial_name_and_path(view, partial_path) + @view_controller = view.controller if view.respond_to?(:controller) + set_path_and_variable_name!(partial_path) super(view, @path, true, locals) add_object_to_local_assigns!(object) # This is needed here in order to compile template with knowledge of 'counter' - initialize_counter - + initialize_counter! + # Prepare early. This is a performance optimization for partial collections prepare! end - + def render - ActionController::Base.benchmark("Rendered #{@path}", Logger::DEBUG, false) do + ActionController::Base.benchmark("Rendered #{@path.path_without_format_and_extension}", Logger::DEBUG, false) do @handler.render(self) end end - + def render_member(object) - @locals[@counter_name] += 1 @locals[:object] = @locals[@variable_name] = object - + template = render_template + @locals[@counter_name] += 1 @locals.delete(@variable_name) @locals.delete(:object) - + template end - + def counter=(num) @locals[@counter_name] = num end private + def add_object_to_local_assigns!(object) + @locals[:object] ||= + @locals[@variable_name] ||= + if object.is_a?(ActionView::Base::ObjectWrapper) + object.value + else + object + end || @view_controller.instance_variable_get("@#{variable_name}") + end - def add_object_to_local_assigns!(object) - @locals[:object] ||= - @locals[@variable_name] ||= - if object.is_a?(ActionView::Base::ObjectWrapper) - object.value - else - object - end || @view.controller.instance_variable_get("@#{variable_name}") - end - - def extract_partial_name_and_path(view, partial_path) - path, partial_name = partial_pieces(view, partial_path) - [File.join(path, "_#{partial_name}"), partial_name.split('/').last.split('.').first.to_sym] - end - - def partial_pieces(view, partial_path) - if partial_path.include?('/') - return File.dirname(partial_path), File.basename(partial_path) - else - return view.controller.class.controller_path, partial_path + def set_path_and_variable_name!(partial_path) + if partial_path.include?('/') + @variable_name = File.basename(partial_path) + @path = "#{File.dirname(partial_path)}/_#{@variable_name}" + elsif @view_controller + @variable_name = partial_path + @path = "#{@view_controller.class.controller_path}/_#{@variable_name}" + else + @variable_name = partial_path + @path = "_#{@variable_name}" + end + + @variable_name = @variable_name.sub(/\..*$/, '').to_sym + end + + def initialize_counter! + @counter_name ||= "#{@variable_name}_counter".to_sym + @locals[@counter_name] = 0 end - end - - def initialize_counter - @counter_name ||= "#{@variable_name}_counter".to_sym - @locals[@counter_name] = 0 - end - end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 369526188f..4c3f252c10 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,19 +1,20 @@ module ActionView #:nodoc: class Template #:nodoc: + extend TemplateHandlers attr_accessor :locals - attr_reader :handler, :path, :extension, :filename, :path_without_extension, :method + attr_reader :handler, :path, :extension, :filename, :method def initialize(view, path, use_full_path, locals = {}) @view = view - @finder = @view.finder + @paths = view.view_paths - # Clear the forward slash at the beginning if exists - @path = use_full_path ? path.sub(/^\//, '') : path - @view.first_render ||= @path + @original_path = path + @path = TemplateFile.from_path(path, !use_full_path) + @view.first_render ||= @path.to_s @source = nil # Don't read the source until we know that it is required set_extension_and_file_name(use_full_path) - + @locals = locals || {} @handler = self.class.handler_class_for_extension(@extension).new(@view) end @@ -29,12 +30,16 @@ module ActionView #:nodoc: raise TemplateError.new(self, @view.assigns, e) end end - + def render prepare! @handler.render(self) end + def path_without_extension + @path.path_without_extension + end + def source @source ||= File.read(self.filename) end @@ -44,13 +49,13 @@ module ActionView #:nodoc: end def base_path_for_exception - @finder.find_base_path_for("#{@path_without_extension}.#{@extension}") || @finder.view_paths.first + (@paths.find_load_path_for_path(@path) || @paths.first).to_s end - + def prepare! @view.send :evaluate_assigns @view.current_render_extension = @extension - + if @handler.compilable? @handler.compile_template(self) # compile the given template, if necessary @method = @view.method_names[method_key] # Set the method name for this template and run it @@ -58,70 +63,32 @@ module ActionView #:nodoc: end private - - def set_extension_and_file_name(use_full_path) - @path_without_extension, @extension = @finder.path_and_extension(@path) - if use_full_path - if @extension - @filename = @finder.pick_template(@path_without_extension, @extension) + def set_extension_and_file_name(use_full_path) + @extension = @path.extension + + if use_full_path + unless @extension + @path = @view.send(:template_file_from_name, @path) + raise_missing_template_exception unless @path + @extension = @path.extension + end + + if @path = @paths.find_template_file_for_path(path) + @filename = @path.full_path + @extension = @path.extension + end else - @extension = @finder.pick_template_extension(@path).to_s - raise_missing_template_exception unless @extension - - @filename = @finder.pick_template(@path, @extension) - @extension = @extension.gsub(/^.+\./, '') # strip off any formats + @filename = @path.full_path end - else - @filename = @path - end - - raise_missing_template_exception if @filename.blank? - end - - def raise_missing_template_exception - full_template_path = @path.include?('.') ? @path : "#{@path}.#{@view.template_format}.erb" - display_paths = @finder.view_paths.join(':') - template_type = (@path =~ /layouts/i) ? 'layout' : 'template' - raise(MissingTemplate, "Missing #{template_type} #{full_template_path} in view path #{display_paths}") - end - - # Template Handlers - - @@template_handlers = HashWithIndifferentAccess.new - @@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 self.register_template_handler(extension, klass) - @@template_handlers[extension.to_sym] = klass - TemplateFinder.update_extension_cache_for(extension.to_s) - end - def self.template_handler_extensions - @@template_handlers.keys.map(&:to_s).sort - end - - def self.register_default_template_handler(extension, klass) - register_template_handler(extension, klass) - @@default_template_handlers = klass - end - - def self.handler_class_for_extension(extension) - (extension && @@template_handlers[extension.to_sym]) || @@default_template_handlers - end - - register_default_template_handler :erb, TemplateHandlers::ERB - register_template_handler :rjs, TemplateHandlers::RJS - register_template_handler :builder, TemplateHandlers::Builder + raise_missing_template_exception if @filename.blank? + end - # TODO: Depreciate old template extensions - register_template_handler :rhtml, TemplateHandlers::ERB - register_template_handler :rxml, TemplateHandlers::Builder - + def raise_missing_template_exception + full_template_path = @original_path.include?('.') ? @original_path : "#{@original_path}.#{@view.template_format}.erb" + display_paths = @paths.join(':') + template_type = (@original_path =~ /layouts/i) ? 'layout' : 'template' + raise(MissingTemplate, "Missing #{template_type} #{full_template_path} in view path #{display_paths}") + end end end diff --git a/actionpack/lib/action_view/template_file.rb b/actionpack/lib/action_view/template_file.rb new file mode 100644 index 0000000000..dd66482b3c --- /dev/null +++ b/actionpack/lib/action_view/template_file.rb @@ -0,0 +1,88 @@ +module ActionView #:nodoc: + # TemplateFile abstracts the pattern of querying a file path for its + # path with or without its extension. The path is only the partial path + # from the load path root e.g. "hello/index.html.erb" not + # "app/views/hello/index.html.erb" + class TemplateFile + def self.from_path(path, use_full_path = false) + path.is_a?(self) ? path : new(path, use_full_path) + end + + def self.from_full_path(load_path, full_path) + file = new(full_path.split(load_path).last) + file.load_path = load_path + file.freeze + end + + attr_accessor :load_path, :base_path, :name, :format, :extension + delegate :to_s, :inspect, :to => :path + + def initialize(path, use_full_path = false) + path = path.dup + + # Clear the forward slash in the beginning unless using full path + trim_forward_slash!(path) unless use_full_path + + @base_path, @name, @format, @extension = split(path) + end + + def freeze + @load_path.freeze + @base_path.freeze + @name.freeze + @format.freeze + @extension.freeze + super + end + + def format_and_extension + extensions = [format, extension].compact.join(".") + extensions.blank? ? nil : extensions + end + + def full_path + if load_path + "#{load_path}/#{path}" + else + path + end + end + + def path + base_path.to_s + [name, format, extension].compact.join(".") + end + + def path_without_extension + base_path.to_s + [name, format].compact.join(".") + end + + def path_without_format_and_extension + "#{base_path}#{name}" + end + + def dup_with_extension(extension) + file = dup + file.extension = extension ? extension.to_s : nil + file + end + + private + def trim_forward_slash!(path) + path.sub!(/^\//, '') + end + + # Returns file split into an array + # [base_path, name, format, extension] + def split(file) + if m = file.match(/^(.*\/)?(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) + if m[5] # Mulipart formats + [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] + elsif m[4] # Single format + [m[1], m[2], m[3], m[4]] + else # No format + [m[1], m[2], nil, m[3]] + end + end + end + end +end diff --git a/actionpack/lib/action_view/template_finder.rb b/actionpack/lib/action_view/template_finder.rb deleted file mode 100644 index 83b7e27c09..0000000000 --- a/actionpack/lib/action_view/template_finder.rb +++ /dev/null @@ -1,177 +0,0 @@ -module ActionView #:nodoc: - class TemplateFinder #:nodoc: - - class InvalidViewPath < StandardError #:nodoc: - attr_reader :unprocessed_path - def initialize(path) - @unprocessed_path = path - super("Unprocessed view path found: #{@unprocessed_path.inspect}. Set your view paths with #append_view_path, #prepend_view_path, or #view_paths=.") - end - end - - cattr_reader :processed_view_paths - @@processed_view_paths = Hash.new {|hash, key| hash[key] = []} - - cattr_reader :file_extension_cache - @@file_extension_cache = Hash.new {|hash, key| - hash[key] = Hash.new {|hash, key| hash[key] = []} - } - - class << self #:nodoc: - - # This method is not thread safe. Mutex should be used whenever this is accessed from an instance method - def process_view_paths(*view_paths) - view_paths.flatten.compact.each do |dir| - next if @@processed_view_paths.has_key?(dir) - @@processed_view_paths[dir] = [] - - # - # Dir.glob("#{dir}/**/*/**") reads all the directories in view path and templates inside those directories - # Dir.glob("#{dir}/**") reads templates residing at top level of view path - # - (Dir.glob("#{dir}/**/*/**") | Dir.glob("#{dir}/**")).each do |file| - unless File.directory?(file) - @@processed_view_paths[dir] << file.split(dir).last.sub(/^\//, '') - - # Build extension cache - extension = file.split(".").last - if template_handler_extensions.include?(extension) - key = file.split(dir).last.sub(/^\//, '').sub(/\.(\w+)$/, '') - @@file_extension_cache[dir][key] << extension - end - end - end - end - end - - def update_extension_cache_for(extension) - @@processed_view_paths.keys.each do |dir| - Dir.glob("#{dir}/**/*.#{extension}").each do |file| - key = file.split(dir).last.sub(/^\//, '').sub(/\.(\w+)$/, '') - @@file_extension_cache[dir][key] << extension - end - end - end - - def template_handler_extensions - ActionView::Template.template_handler_extensions - end - - def reload! - view_paths = @@processed_view_paths.keys - - @@processed_view_paths = Hash.new {|hash, key| hash[key] = []} - @@file_extension_cache = Hash.new {|hash, key| - hash[key] = Hash.new {|hash, key| hash[key] = []} - } - - process_view_paths(view_paths) - end - end - - attr_accessor :view_paths - - def initialize(*args) - @template = args.shift - - @view_paths = args.flatten - @view_paths = @view_paths.respond_to?(:find) ? @view_paths.dup : [*@view_paths].compact - check_view_paths(@view_paths) - end - - def prepend_view_path(path) - @view_paths.unshift(*path) - - self.class.process_view_paths(path) - end - - def append_view_path(path) - @view_paths.push(*path) - - self.class.process_view_paths(path) - end - - def view_paths=(path) - @view_paths = path - self.class.process_view_paths(path) - end - - def pick_template(template_path, extension) - file_name = "#{template_path}.#{extension}" - base_path = find_base_path_for(file_name) - base_path.blank? ? false : "#{base_path}/#{file_name}" - end - alias_method :template_exists?, :pick_template - - def file_exists?(template_path) - # Clear the forward slash in the beginning if exists - template_path = template_path.sub(/^\//, '') - - template_file_name, template_file_extension = path_and_extension(template_path) - - if template_file_extension - template_exists?(template_file_name, template_file_extension) - else - template_exists?(template_file_name, pick_template_extension(template_path)) - end - end - - def find_base_path_for(template_file_name) - @view_paths.find { |path| @@processed_view_paths[path].include?(template_file_name) } - end - - # Returns the view path that the full path resides in. - def extract_base_path_from(full_path) - @view_paths.find { |p| full_path[0..p.size - 1] == p } - end - - # Gets the extension for an existing template with the given template_path. - # Returns the format with the extension if that template exists. - # - # pick_template_extension('users/show') - # # => 'html.erb' - # - # pick_template_extension('users/legacy') - # # => "rhtml" - # - def pick_template_extension(template_path) - if extension = find_template_extension_from_handler(template_path, @template.template_format) || find_template_extension_from_first_render - extension - elsif @template.template_format == :js && extension = find_template_extension_from_handler(template_path, :html) - @template.template_format = :html - extension - end - end - - def find_template_extension_from_handler(template_path, template_format = @template.template_format) - formatted_template_path = "#{template_path}.#{template_format}" - - view_paths.each do |path| - if (extensions = @@file_extension_cache[path][formatted_template_path]).any? - return "#{template_format}.#{extensions.first}" - elsif (extensions = @@file_extension_cache[path][template_path]).any? - return extensions.first.to_s - end - end - nil - end - - # Splits the path and extension from the given template_path and returns as an array. - def path_and_extension(template_path) - template_path_without_extension = template_path.sub(/\.(\w+)$/, '') - [ template_path_without_extension, $1 ] - end - - # Determine the template extension from the <tt>@first_render</tt> filename - def find_template_extension_from_first_render - File.basename(@template.first_render.to_s)[/^[^.]+\.(.+)$/, 1] - end - - private - def check_view_paths(view_paths) - view_paths.each do |path| - raise InvalidViewPath.new(path) unless @@processed_view_paths.has_key?(path) - end - end - end -end diff --git a/actionpack/lib/action_view/template_handler.rb b/actionpack/lib/action_view/template_handler.rb index ec407e3fb3..39e578e586 100644 --- a/actionpack/lib/action_view/template_handler.rb +++ b/actionpack/lib/action_view/template_handler.rb @@ -1,6 +1,5 @@ module ActionView class TemplateHandler - def self.line_offset 0 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..1471e99e01 --- /dev/null +++ b/actionpack/lib/action_view/template_handlers.rb @@ -0,0 +1,46 @@ +require 'action_view/template_handler' +require 'action_view/template_handlers/compilable' +require 'action_view/template_handlers/builder' +require 'action_view/template_handlers/erb' +require 'action_view/template_handlers/rjs' + +module ActionView #:nodoc: + module TemplateHandlers #:nodoc: + 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 register_default_template_handler(extension, klass) + register_template_handler(extension, klass) + @@default_template_handlers = klass + end + + def handler_class_for_extension(extension) + (extension && @@template_handlers[extension.to_sym]) || @@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 index f76d89777a..ee02ce1a6f 100644 --- a/actionpack/lib/action_view/template_handlers/builder.rb +++ b/actionpack/lib/action_view/template_handlers/builder.rb @@ -11,10 +11,11 @@ module ActionView def compile(template) content_type_handler = (@view.send!(:controller).respond_to?(:response) ? "controller.response" : "controller") + "#{content_type_handler}.content_type ||= Mime::XML\n" + - "xml = ::Builder::XmlMarkup.new(:indent => 2)\n" + - template.source + - "\nxml.target!\n" + "xml = ::Builder::XmlMarkup.new(:indent => 2)\n" + + template.source + + "\nxml.target!\n" end def cache_fragment(block, name = {}, options = nil) diff --git a/actionpack/lib/action_view/template_handlers/compilable.rb b/actionpack/lib/action_view/template_handlers/compilable.rb index 25bd0fea7f..783456ab53 100644 --- a/actionpack/lib/action_view/template_handlers/compilable.rb +++ b/actionpack/lib/action_view/template_handlers/compilable.rb @@ -106,7 +106,13 @@ module ActionView locals_code << "#{key} = local_assigns[:#{key}]\n" end - "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" + <<-end_src + def #{render_symbol}(local_assigns) + old_output_buffer = output_buffer;#{locals_code}#{body} + ensure + self.output_buffer = old_output_buffer + end + end_src end # Return true if the given template was compiled for a superset of the keys in local_assigns @@ -125,4 +131,4 @@ module ActionView end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/template_handlers/erb.rb b/actionpack/lib/action_view/template_handlers/erb.rb index 15a9064461..ac715e30c2 100644 --- a/actionpack/lib/action_view/template_handlers/erb.rb +++ b/actionpack/lib/action_view/template_handlers/erb.rb @@ -42,13 +42,19 @@ module ActionView class ERB < TemplateHandler include Compilable + # 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) - ::ERB.new(template.source, nil, @view.erb_trim_mode).src + src = ::ERB.new(template.source, nil, erb_trim_mode, '@output_buffer').src + "__in_erb_template=true;#{src}" end def cache_fragment(block, name = {}, options = nil) #:nodoc: @view.fragment_for(block, name, options) do - eval(ActionView::Base.erb_variable, block.binding) + @view.response.template.output_buffer end end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 16fedd9732..1a3c93c283 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -37,6 +37,8 @@ module ActionView if helper_class && !self.class.ancestors.include?(helper_class) self.class.send(:include, helper_class) end + + self.output_buffer = '' end class TestController < ActionController::Base @@ -48,6 +50,9 @@ module ActionView end end + protected + attr_accessor :output_buffer + private def method_missing(selector, *args) controller = TestController.new diff --git a/actionpack/lib/action_view/view_load_paths.rb b/actionpack/lib/action_view/view_load_paths.rb new file mode 100644 index 0000000000..e873d96aa0 --- /dev/null +++ b/actionpack/lib/action_view/view_load_paths.rb @@ -0,0 +1,103 @@ +module ActionView #:nodoc: + class ViewLoadPaths < Array #:nodoc: + def self.type_cast(obj) + obj.is_a?(String) ? LoadPath.new(obj) : obj + end + + class LoadPath #:nodoc: + attr_reader :path, :paths + delegate :to_s, :inspect, :to => :path + + def initialize(path) + @path = path.freeze + reload! + end + + def eql?(view_path) + view_path.is_a?(ViewPath) && @path == view_path.path + end + + def hash + @path.hash + end + + # Rebuild load path directory cache + def reload! + @paths = {} + + files.each do |file| + @paths[file.path] = file + @paths[file.path_without_extension] ||= file + end + + @paths.freeze + end + + # Tries to find the extension for the template name. + # If it does not it exist, tries again without the format extension + # find_template_file_for_partial_path('users/show') => 'html.erb' + # find_template_file_for_partial_path('users/legacy') => 'rhtml' + def find_template_file_for_partial_path(file) + @paths[file.path] || @paths[file.path_without_extension] || @paths[file.path_without_format_and_extension] + end + + private + # Get all the files and directories in the path + def files_in_path + Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**") + end + + # Create an array of all the files within the path + def files + files_in_path.map do |file| + TemplateFile.from_full_path(@path, file) unless File.directory?(file) + end.compact + end + end + + def initialize(*args) + super(*args).map! { |obj| self.class.type_cast(obj) } + end + + def reload! + each { |path| path.reload! } + end + + def <<(obj) + super(self.class.type_cast(obj)) + end + + def push(*objs) + delete_paths!(objs) + super(*objs.map { |obj| self.class.type_cast(obj) }) + end + + def unshift(*objs) + delete_paths!(objs) + super(*objs.map { |obj| self.class.type_cast(obj) }) + end + + def template_exists?(file) + find_load_path_for_path(file) ? true : false + end + + def find_load_path_for_path(file) + find { |path| path.paths[file.to_s] } + end + + def find_template_file_for_path(file) + file = TemplateFile.from_path(file) + each do |path| + if f = path.find_template_file_for_partial_path(file) + return f + end + end + nil + end + + private + def delete_paths!(paths) + paths.each { |p1| delete_if { |p2| p1.to_s == p2.to_s } } + end + end +end |