diff options
author | Xavier Noria <fxn@hashref.com> | 2010-05-27 10:57:05 +0200 |
---|---|---|
committer | Xavier Noria <fxn@hashref.com> | 2010-05-27 10:57:05 +0200 |
commit | ddec74fb3ab66528ca98ff37ca9b5f4227fdd2e3 (patch) | |
tree | c58ac9162487bdca68342fb2d30730ccfd926378 | |
parent | 96e2094b8b634e4af0d9d3c8a1db9bbb7023a4a7 (diff) | |
parent | e02db06ece7aeecec7c37f5b0e3de7d65c8684e6 (diff) | |
download | rails-ddec74fb3ab66528ca98ff37ca9b5f4227fdd2e3.tar.gz rails-ddec74fb3ab66528ca98ff37ca9b5f4227fdd2e3.tar.bz2 rails-ddec74fb3ab66528ca98ff37ca9b5f4227fdd2e3.zip |
Merge remote branch 'rails/master'
54 files changed, 1120 insertions, 1020 deletions
@@ -17,15 +17,25 @@ if mri || RUBY_ENGINE == "rbx" gem 'yajl-ruby' gem "nokogiri", ">= 1.4.0" elsif RUBY_ENGINE == "jruby" - gem "jruby-debug" + gem "ruby-debug" + gem "jruby-openssl" end # AR -gem "sqlite3-ruby", "= 1.3.0.beta.2", :require => 'sqlite3' +if mri || RUBY_ENGINE == "rbx" + gem "sqlite3-ruby", "= 1.3.0.beta.2", :require => 'sqlite3' + + group :db do + gem "pg", ">= 0.9.0" + gem "mysql", ">= 2.8.1" + end +elsif RUBY_ENGINE == "jruby" + gem "activerecord-jdbcsqlite3-adapter" -group :db do - gem "pg", ">= 0.9.0" - gem "mysql", ">= 2.8.1" + group :db do + gem "activerecord-jdbcmysql-adapter" + gem "activerecord-jdbcpostgresql-adapter" + end end # AP @@ -66,6 +66,7 @@ Rake::RDocTask.new do |rdoc| rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '-A cattr_accessor=object' rdoc.options << '--charset' << 'utf-8' + rdoc.options << '--main' << 'railties/README' rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : './doc/template/horo' diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 208299cfd1..7a17ff1df9 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,7 +1,10 @@ +*Rails 3.0.0 [beta 4/release candidate] (unreleased)* + * Changed encoding behaviour of mail, so updated tests in actionmailer and bumped mail version to 2.2.1 [ML] * Added ability to pass Proc objects to the defaults hash [ML] + *Rails 3.0.0 [beta 3] (April 13th, 2010)* * Removed all quoting.rb type files from ActionMailer and put Mail 2.2.0 in instead [ML] diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 319472c937..5cd7a90ab5 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -245,7 +245,7 @@ module AbstractController case defined?(@_layout) ? @_layout : nil when String - self.class_eval %{def _layout; #{@_layout.inspect} end} + self.class_eval %{def _layout; #{@_layout.inspect} end}, __FILE__, __LINE__ when Symbol self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 def _layout @@ -259,9 +259,9 @@ module AbstractController ruby_eval when Proc define_method :_layout_from_proc, &@_layout - self.class_eval %{def _layout; _layout_from_proc(self) end} + self.class_eval %{def _layout; _layout_from_proc(self) end}, __FILE__, __LINE__ when false - self.class_eval %{def _layout; end} + self.class_eval %{def _layout; end}, __FILE__, __LINE__ when true raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" when nil diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 6ad9a23542..22bdcd0f3c 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -217,7 +217,7 @@ module ActionController #:nodoc: # the verb is POST. # def default_action - @action ||= ACTIONS_FOR_VERBS[request.method_symbol] + @action ||= ACTIONS_FOR_VERBS[request.request_method_symbol] end end end diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index d9f09b5dc5..0cddd09eb0 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -43,7 +43,7 @@ module ActionView end %w(tag content_tag to_date_select_tag to_datetime_select_tag to_time_select_tag).each do |meth| - module_eval "def #{meth}(*) error_wrapping(super) end" + module_eval "def #{meth}(*) error_wrapping(super) end", __FILE__, __LINE__ end def error_wrapping(html_tag) diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 414a5d4cd9..b3db3151d3 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -1111,7 +1111,7 @@ module ActionView end (field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector| - src, line = <<-end_src, __LINE__ + 1 + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( #{selector.inspect}, # "text_field", @@ -1119,8 +1119,7 @@ module ActionView method, # method, objectify_options(options)) # objectify_options(options)) end # end - end_src - class_eval src, __FILE__, line + RUBY_EVAL end def fields_for(record_or_name_or_array, *args, &block) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 2a3f826c15..796268628a 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -293,7 +293,7 @@ module ActionView escape = options.key?("escape") ? options.delete("escape") : true content = html_escape(content) if escape - content_tag :textarea, content.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options) + content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options) end # Creates a check box form input tag. diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 086ad261c8..0d2b2aa7b1 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -3,7 +3,7 @@ require 'action_view/helpers/tag_helper' module ActionView module Helpers module TranslationHelper - # Delegates to I18n#translate but also performs three additional functions. First, it'll catch MissingTranslationData exceptions + # Delegates to I18n#translate but also performs three additional functions. First, it'll catch MissingTranslationData exceptions # and turn them into inline spans that contains the missing key, such that you can see in a view what is missing where. # # Second, it'll scope the key by the current partial if the key starts with a period. So if you call translate(".foo") from the @@ -11,16 +11,14 @@ module ActionView # to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don't # prepend the key with a period, nothing is converted. # - # Third, it’ll mark the translation as safe HTML if the key has the suffix "_html" or the last element of the key is the word - # "html". For example, calling translate("footer_html") or translate("footer.html") will return a safe HTML string that won’t - # be escaped by other HTML helper methods. This naming convention helps to identify translations that include HTML tags so that + # Third, it’ll mark the translation as safe HTML if the key has the suffix "_html" or the last element of the key is the word + # "html". For example, calling translate("footer_html") or translate("footer.html") will return a safe HTML string that won’t + # be escaped by other HTML helper methods. This naming convention helps to identify translations that include HTML tags so that # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) - options[:raise] = true - translation = I18n.translate(scope_key_by_partial(key), options) - translation = (translation.respond_to?(:join) ? translation.join : translation) - if html_safe_translation_key?(key) + translation = I18n.translate(scope_key_by_partial(key), options.merge!(:raise => true)) + if html_safe_translation_key?(key) && translation.respond_to?(:html_safe) translation.html_safe else translation @@ -38,12 +36,10 @@ module ActionView alias :l :localize private - def scope_key_by_partial(key) - strkey = key.respond_to?(:join) ? key.join : key.to_s - if strkey.first == "." + if key.to_s.first == "." if @_virtual_path - @_virtual_path.gsub(%r{/_?}, ".") + strkey + @_virtual_path.gsub(%r{/_?}, ".") + key.to_s else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end @@ -53,8 +49,7 @@ module ActionView end def html_safe_translation_key?(key) - key = key.is_a?(Array) ? key.last : key.to_s - key =~ /(\b|_|\.)html$/ + key.to_s =~ /(\b|_|\.)html$/ end end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index f6417d5c2c..4dbbd2eb6a 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -10,11 +10,16 @@ module ActionView attr_accessor :request, :response, :params - def self.controller_path - '' + class << self + attr_writer :controller_path + end + + def controller_path=(path) + self.class.controller_path=(path) end def initialize + self.class.controller_path = "" @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @@ -24,80 +29,99 @@ module ActionView end end - include ActionDispatch::Assertions, ActionDispatch::TestProcess - include ActionController::TemplateAssertions - include ActionView::Context + module Behavior + extend ActiveSupport::Concern - include ActionController::PolymorphicRoutes - include ActionController::RecordIdentifier + include ActionDispatch::Assertions, ActionDispatch::TestProcess + include ActionController::TemplateAssertions + include ActionView::Context - include AbstractController::Helpers - include ActionView::Helpers + include ActionController::PolymorphicRoutes + include ActionController::RecordIdentifier - class_inheritable_accessor :helper_class - attr_accessor :controller, :output_buffer, :rendered + include AbstractController::Helpers + include ActionView::Helpers - setup :setup_with_controller - def setup_with_controller - @controller = TestController.new - @output_buffer = ActiveSupport::SafeBuffer.new - @rendered = '' + attr_accessor :controller, :output_buffer, :rendered + + module ClassMethods + def tests(helper_class) + self.helper_class = helper_class + end - self.class.send(:include_helper_modules!) - make_test_case_available_to_view! - end + def determine_default_helper_class(name) + mod = name.sub(/Test$/, '').constantize + mod.is_a?(Class) ? nil : mod + rescue NameError + nil + end - def config - @controller.config if @controller.respond_to?(:config) - end + def helper_method(*methods) + # Almost a duplicate from ActionController::Helpers + methods.flatten.each do |method| + _helpers.module_eval <<-end_eval + def #{method}(*args, &block) # def current_user(*args, &block) + _test_case.send(%(#{method}), *args, &block) # test_case.send(%(current_user), *args, &block) + end # end + end_eval + end + end - def render(options = {}, local_assigns = {}, &block) - @rendered << output = _view.render(options, local_assigns, &block) - output - end + attr_writer :helper_class - def protect_against_forgery? - false - end + def helper_class + @helper_class ||= determine_default_helper_class(name) + end - class << self - def tests(helper_class) - self.helper_class = helper_class - end + private - def helper_class - if current_helper_class = read_inheritable_attribute(:helper_class) - current_helper_class - else - self.helper_class = determine_default_helper_class(name) + def include_helper_modules! + helper(helper_class) if helper_class + include _helpers end + end - def determine_default_helper_class(name) - name.sub(/Test$/, '').constantize - rescue NameError - nil + def setup_with_controller + @controller = ActionView::TestCase::TestController.new + @output_buffer = ActiveSupport::SafeBuffer.new + @rendered = '' + + self.class.send(:include_helper_modules!) + make_test_case_available_to_view! + say_no_to_protect_against_forgery! end - def helper_method(*methods) - # Almost a duplicate from ActionController::Helpers - methods.flatten.each do |method| - _helpers.module_eval <<-end_eval - def #{method}(*args, &block) # def current_user(*args, &block) - _test_case.send(%(#{method}), *args, &block) # test_case.send(%(current_user), *args, &block) - end # end - end_eval - end + def config + @controller.config if @controller.respond_to?(:config) end - private - def include_helper_modules! - helper(helper_class) if helper_class - include _helpers - end - end + def render(options = {}, local_assigns = {}, &block) + @rendered << output = _view.render(options, local_assigns, &block) + output + end + + included do + setup :setup_with_controller + end private + + # Support the selector assertions + # + # Need to experiment if this priority is the best one: rendered => output_buffer + def response_from_page_or_rjs + HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root + end + + def say_no_to_protect_against_forgery! + _helpers.module_eval do + def protect_against_forgery? + false + end + end + end + def make_test_case_available_to_view! test_case_instance = self _helpers.module_eval do @@ -110,29 +134,30 @@ module ActionView view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller) view.singleton_class.send :include, _helpers view.singleton_class.send :include, @controller._router.url_helpers + view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash" view.output_buffer = self.output_buffer view end - # Support the selector assertions - # - # Need to experiment if this priority is the best one: rendered => output_buffer - def response_from_page_or_rjs - HTML::Document.new(rendered.blank? ? output_buffer : rendered).root - end - EXCLUDE_IVARS = %w{ + @_result @output_buffer - @fixture_cache + @rendered + @templates + @view_context_class + @layouts + @partials + @controller + @method_name - @_result + @fixture_cache @loaded_fixtures @test_passed - @view } def _instance_variables instance_variables - EXCLUDE_IVARS + instance_variables end def _assigns @@ -155,5 +180,10 @@ module ActionView super end end + + end + + include Behavior + end end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index c8ba8bcaf3..d654338dba 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -668,6 +668,19 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override + with_test_route_set do + errors = { :name => :invalid } + Customer.any_instance.stubs(:errors).returns(errors) + @request.env["rack.methodoverride.original_method"] = "POST" + put :using_resource + assert_equal "text/html", @response.content_type + assert_equal 200, @response.status + assert_equal "Edit world!\n", @response.body + assert_nil @response.location + end + end + def test_using_resource_for_put_with_xml_yields_ok_on_success @request.accept = "application/xml" put :using_resource diff --git a/actionpack/test/fixtures/test/array_translation.erb b/actionpack/test/fixtures/test/array_translation.erb deleted file mode 100644 index def3a1a0c1..0000000000 --- a/actionpack/test/fixtures/test/array_translation.erb +++ /dev/null @@ -1 +0,0 @@ -<%= t(['foo', 'bar', 'html']) %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/scoped_array_translation.erb b/actionpack/test/fixtures/test/scoped_array_translation.erb deleted file mode 100644 index 0a0c79f717..0000000000 --- a/actionpack/test/fixtures/test/scoped_array_translation.erb +++ /dev/null @@ -1 +0,0 @@ -<%= t(['.foo', '.bar']) %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/scoped_translation.erb b/actionpack/test/fixtures/test/scoped_translation.erb new file mode 100644 index 0000000000..30baa61c77 --- /dev/null +++ b/actionpack/test/fixtures/test/scoped_translation.erb @@ -0,0 +1 @@ +<%= t('.foo.bar') %>
\ No newline at end of file diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index d1e1338a17..2f3869994c 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1425,12 +1425,11 @@ class FormHelperTest < ActionView::TestCase class LabelledFormBuilder < ActionView::Helpers::FormBuilder (field_helpers - %w(hidden_field)).each do |selector| - src, line = <<-END_SRC, __LINE__ + 1 + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(field, *args, &proc) ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe end - END_SRC - class_eval src, __FILE__, line + RUBY_EVAL end end diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 1c095b621e..b75863bb6a 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -206,6 +206,12 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_text_area_tag_unescaped_nil_content + actual = text_area_tag "body", nil, :escape => false + expected = %(<textarea id="body" name="body"></textarea>) + assert_dom_equal expected, actual + end + def test_text_field_tag actual = text_field_tag "title", "Hello!" expected = %(<input id="title" name="title" type="text" value="Hello!" />) diff --git a/actionpack/test/template/test_case_test.rb b/actionpack/test/template/test_case_test.rb index c1a38a25de..16e5ee4f72 100644 --- a/actionpack/test/template/test_case_test.rb +++ b/actionpack/test/template/test_case_test.rb @@ -2,21 +2,23 @@ require 'abstract_unit' require 'controller/fake_controllers' module ActionView - class TestCase - module ATestHelper - end - module AnotherTestHelper - def from_another_helper - 'Howdy!' - end + module ATestHelper + end + + module AnotherTestHelper + def from_another_helper + 'Howdy!' end + end - module ASharedTestHelper - def from_shared_helper - 'Holla!' - end + module ASharedTestHelper + def from_shared_helper + 'Holla!' end + end + + class TestCase helper ASharedTestHelper module SharedTests @@ -29,161 +31,186 @@ module ActionView end end end + end - class GeneralViewTest < ActionView::TestCase - include SharedTests - test_case = self + class GeneralViewTest < ActionView::TestCase + include SharedTests + test_case = self - test "works without testing a helper module" do - assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) - end + test "works without testing a helper module" do + assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) + end - test "can render a layout with block" do - assert_equal "Before (ChrisCruft)\n!\nAfter", - render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"} - end + test "can render a layout with block" do + assert_equal "Before (ChrisCruft)\n!\nAfter", + render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"} + end - helper AnotherTestHelper - test "additional helper classes can be specified as in a controller" do - assert test_case.ancestors.include?(AnotherTestHelper) - assert_equal 'Howdy!', from_another_helper - end + helper AnotherTestHelper + test "additional helper classes can be specified as in a controller" do + assert test_case.ancestors.include?(AnotherTestHelper) + assert_equal 'Howdy!', from_another_helper + end + + test "determine_default_helper_class returns nil if name.sub(/Test$/, '').constantize resolves to a class" do + assert_nil self.class.determine_default_helper_class("String") end - class ClassMethodsTest < ActionView::TestCase - include SharedTests - test_case = self + test "delegates notice to request.flash" do + _view.request.flash.expects(:notice).with("this message") + _view.notice("this message") + end - tests ATestHelper - test "tests the specified helper module" do - assert_equal ATestHelper, test_case.helper_class - assert test_case.ancestors.include?(ATestHelper) - end + test "delegates alert to request.flash" do + _view.request.flash.expects(:alert).with("this message") + _view.alert("this message") + end + end - helper AnotherTestHelper - test "additional helper classes can be specified as in a controller" do - assert test_case.ancestors.include?(AnotherTestHelper) - assert_equal 'Howdy!', from_another_helper + class ClassMethodsTest < ActionView::TestCase + include SharedTests + test_case = self - test_case.helper_class.module_eval do - def render_from_helper - from_another_helper - end - end - assert_equal 'Howdy!', render(:partial => 'test/from_helper') - end + tests ATestHelper + test "tests the specified helper module" do + assert_equal ATestHelper, test_case.helper_class + assert test_case.ancestors.include?(ATestHelper) end - class ATestHelperTest < ActionView::TestCase - include SharedTests - test_case = self + helper AnotherTestHelper + test "additional helper classes can be specified as in a controller" do + assert test_case.ancestors.include?(AnotherTestHelper) + assert_equal 'Howdy!', from_another_helper - test "inflects the name of the helper module to test from the test case class" do - assert_equal ATestHelper, test_case.helper_class - assert test_case.ancestors.include?(ATestHelper) + test_case.helper_class.module_eval do + def render_from_helper + from_another_helper + end end + assert_equal 'Howdy!', render(:partial => 'test/from_helper') + end + end - test "a configured test controller is available" do - assert_kind_of ActionController::Base, controller - assert_equal '', controller.controller_path + class HelperInclusionTest < ActionView::TestCase + module RenderHelper + def render_from_helper + render :partial => 'customer', :collection => @customers end + end - test "helper class that is being tested is always included in view instance" do - # This ensure is a hidious hack to deal with these tests bleeding - # methods between eachother - begin - self.class.helper_class.module_eval do - def render_from_helper - render :partial => 'customer', :collection => @customers - end - end + helper RenderHelper - TestController.stubs(:controller_path).returns('test') + test "helper class that is being tested is always included in view instance" do + @controller.controller_path = 'test' - @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] - assert_match /Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper') + @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + assert_match /Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper') + end + end - ensure - self.class.helper_class.send(:remove_method, :render_from_helper) - end + class HelperExposureTest < ActionView::TestCase + helper(Module.new do + def render_from_helper + from_test_case end + end) + test "is able to make methods available to the view" do + assert_equal 'Word!', render(:partial => 'test/from_helper') + end - test "no additional helpers should shared across test cases" do - assert !test_case.ancestors.include?(AnotherTestHelper) - assert_raise(NoMethodError) { send :from_another_helper } - end + def from_test_case; 'Word!'; end + helper_method :from_test_case + end - test "is able to use routes" do - controller.request.assign_parameters(@routes, 'foo', 'index') - assert_equal '/foo', url_for - assert_equal '/bar', url_for(:controller => 'bar') + class IgnoreProtectAgainstForgeryTest < ActionView::TestCase + module HelperThatInvokesProtectAgainstForgery + def help_me + protect_against_forgery? end + end - test "is able to use named routes" do - with_routing do |set| - set.draw { |map| resources :contents } - assert_equal 'http://test.host/contents/new', new_content_url - assert_equal 'http://test.host/contents/1', content_url(:id => 1) - end - end + helper HelperThatInvokesProtectAgainstForgery - test "named routes can be used from helper included in view" do - with_routing do |set| - set.draw { |map| resources :contents } - _helpers.module_eval do - def render_from_helper - new_content_url - end - end + test "protect_from_forgery? in any helpers returns false" do + assert !_view.help_me + end - assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper') - end - end + end - test "is able to render partials with local variables" do - assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) - assert_equal 'Eloy', render(:partial => 'developers/developer', - :locals => { :developer => stub(:name => 'Eloy') }) - end + class ATestHelperTest < ActionView::TestCase + include SharedTests + test_case = self + + test "inflects the name of the helper module to test from the test case class" do + assert_equal ATestHelper, test_case.helper_class + assert test_case.ancestors.include?(ATestHelper) + end + + test "a configured test controller is available" do + assert_kind_of ActionController::Base, controller + assert_equal '', controller.controller_path + end - test "is able to render partials from templates and also use instance variables" do - TestController.stubs(:controller_path).returns('test') + test "no additional helpers should shared across test cases" do + assert !test_case.ancestors.include?(AnotherTestHelper) + assert_raise(NoMethodError) { send :from_another_helper } + end - @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] - assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') + test "is able to use routes" do + controller.request.assign_parameters(@routes, 'foo', 'index') + assert_equal '/foo', url_for + assert_equal '/bar', url_for(:controller => 'bar') + end + + test "is able to use named routes" do + with_routing do |set| + set.draw { |map| resources :contents } + assert_equal 'http://test.host/contents/new', new_content_url + assert_equal 'http://test.host/contents/1', content_url(:id => 1) end + end - test "is able to make methods available to the view" do - # This ensure is a hidious hack to deal with these tests bleeding - # methods between eachother - begin - _helpers.module_eval do - def render_from_helper; from_test_case end + test "named routes can be used from helper included in view" do + with_routing do |set| + set.draw { |map| resources :contents } + _helpers.module_eval do + def render_from_helper + new_content_url end - assert_equal 'Word!', render(:partial => 'test/from_helper') - ensure - _helpers.send(:remove_method, :render_from_helper) end + + assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper') end + end - def from_test_case; 'Word!'; end - helper_method :from_test_case + test "is able to render partials with local variables" do + assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) + assert_equal 'Eloy', render(:partial => 'developers/developer', + :locals => { :developer => stub(:name => 'Eloy') }) end - class AssertionsTest < ActionView::TestCase - def render_from_helper - form_tag('/foo') do - safe_concat render(:text => '<ul><li>foo</li></ul>') - end + test "is able to render partials from templates and also use instance variables" do + @controller.controller_path = "test" + + @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') + end + + end + + class AssertionsTest < ActionView::TestCase + def render_from_helper + form_tag('/foo') do + safe_concat render(:text => '<ul><li>foo</li></ul>') end - helper_method :render_from_helper + end + helper_method :render_from_helper - test "uses the output_buffer for assert_select" do - render(:partial => 'test/from_helper') + test "uses the output_buffer for assert_select" do + render(:partial => 'test/from_helper') - assert_select 'form' do - assert_select 'li', :text => 'foo' - end + assert_select 'form' do + assert_select 'li', :text => 'foo' end end end diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index b382b5eb22..1be418a206 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -18,16 +18,9 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal expected, translate(:foo) end - def test_translation_of_an_array - I18n.expects(:translate).with(["foo", "bar"], :raise => true).returns(["foo", "bar"]) - assert_equal "foobar", translate(["foo", "bar"]) - end - - def test_translation_of_an_array_with_html - expected = '<a href="#">foo</a><a href="#">bar</a>' - I18n.expects(:translate).with(["foo", "bar", "html"], :raise => true).returns(['<a href="#">foo</a>', '<a href="#">bar</a>']) - @view = ActionView::Base.new(ActionController::Base.view_paths, {}) - assert_equal expected, @view.render(:file => "test/array_translation") + def test_translation_returning_an_array + I18n.expects(:translate).with(:foo, :raise => true).returns(["foo", "bar"]) + assert_equal ["foo", "bar"], translate(:foo) end def test_delegates_localize_to_i18n @@ -43,9 +36,9 @@ class TranslationHelperTest < ActiveSupport::TestCase end def test_scoping_by_partial_of_an_array - I18n.expects(:translate).with("test.scoped_array_translation.foo.bar", :raise => true).returns(["foo", "bar"]) + I18n.expects(:translate).with("test.scoped_translation.foo.bar", :raise => true).returns(["foo", "bar"]) @view = ActionView::Base.new(ActionController::Base.view_paths, {}) - assert_equal "foobar", @view.render(:file => "test/scoped_array_translation") + assert_equal "foobar", @view.render(:file => "test/scoped_translation") end def test_translate_does_not_mark_plain_text_as_safe_html @@ -62,4 +55,9 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.expects(:translate).with("hello_html", :raise => true).returns("<a>Hello World</a>") assert translate("hello_html").html_safe? end + + def test_translation_returning_an_array_ignores_html_suffix + I18n.expects(:translate).with(:foo_html, :raise => true).returns(["foo", "bar"]) + assert_equal ["foo", "bar"], translate(:foo_html) + end end diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 95bbaf00cf..1f5217191c 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -378,7 +378,7 @@ module ActiveRecord def interpolate_sql_for_preload(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@") + instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end def append_conditions(reflection, preload_options) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f23d881c7b..5b0ba86308 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1461,7 +1461,7 @@ module ActiveRecord before_destroy(method_name) module_eval( - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)" + "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ ) end @@ -2130,7 +2130,7 @@ module ActiveRecord end def interpolate_sql(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@") + instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1b76f357e3..aa2826fb33 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1786,7 +1786,7 @@ module ActiveRecord #:nodoc: # Interpolate custom SQL string in instance context. # Optional record argument is meant for custom insert_sql. def interpolate_sql(sql, record = nil) - instance_eval("%@#{sql.gsub('@', '\@')}@") + instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 8473150338..329dd7d761 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -287,11 +287,8 @@ class CalculationsTest < ActiveRecord::TestCase # Oracle adapter returns floating point value 636.0 after SUM if current_adapter?(:OracleAdapter) assert_equal 636, Account.sum("2 * credit_limit") - elsif current_adapter?(:SQLite3Adapter) - # Future versions of the SQLite3 adapter will return a number - assert_equal 636, Account.sum("2 * credit_limit").to_i else - assert_equal '636', Account.sum("2 * credit_limit") + assert_equal 636, Account.sum("2 * credit_limit").to_i end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 3a34df2426..9527404f03 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -63,19 +63,19 @@ class ValidationsTest < ActiveRecord::TestCase end def test_error_on_given_context - r = WrongReply.new + r = WrongReply.new(:title => "Valid title") assert !r.valid?(:special_case) - assert "Invalid", r.errors[:title].join + assert_equal "Invalid", r.errors[:author_name].join - r.title = "secret" + r.author_name = "secret" r.content = "Good" assert r.valid?(:special_case) - r.title = nil + r.author_name = nil assert !r.save(:context => :special_case) - assert "Invalid", r.errors[:title].join + assert_equal "Invalid", r.errors[:author_name].join - r.title = "secret" + r.author_name = "secret" assert r.save(:context => :special_case) end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 6cc9ee038a..110d540120 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -17,7 +17,7 @@ class WrongReply < Reply validate :check_empty_title validate :check_content_mismatch, :on => :create validate :check_wrong_update, :on => :update - validate :check_title_is_secret, :on => :special_case + validate :check_author_name_is_secret, :on => :special_case def check_empty_title errors[:title] << "Empty" unless attribute_present?("title") @@ -41,8 +41,8 @@ class WrongReply < Reply errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" end - def check_title_is_secret - errors[:title] << "Invalid" unless title == "secret" + def check_author_name_is_secret + errors[:author_name] << "Invalid" unless author_name == "secret" end end diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index b976844c1c..b89097ac4b 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -582,12 +582,13 @@ module ActiveResource # Clear prefix parameters in case they have been cached @prefix_parameters = nil - # Redefine the new methods. - code, line = <<-end_code, __LINE__ + 1 - def prefix_source() "#{value}" end - def prefix(options={}) "#{prefix_call}" end - end_code - silence_warnings { instance_eval code, __FILE__, line } + silence_warnings do + # Redefine the new methods. + instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def prefix_source() "#{value}" end + def prefix(options={}) "#{prefix_call}" end + RUBY_EVAL + end rescue logger.error "Couldn't set prefix: #{$!}\n #{code}" if logger raise @@ -1043,11 +1044,6 @@ module ActiveResource attributes[self.class.primary_key] = id end - # Allows Active Resource objects to be used as parameters in Action Pack URL generation. - def to_param - id && id.to_s - end - # Test for equality. Resource are equal if and only if +other+ is the same object or # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+. # @@ -1410,6 +1406,7 @@ module ActiveResource class Base extend ActiveModel::Naming include CustomMethods, Observing, Validations + include ActiveModel::Conversion include ActiveModel::Serializers::JSON include ActiveModel::Serializers::Xml end diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index d5ee7659af..35c3f4c6ef 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -1017,7 +1017,7 @@ class BaseTest < Test::Unit::TestCase encode = matz.encode xml = matz.to_xml - assert encode, xml + assert_equal encode, xml assert xml.include?('<?xml version="1.0" encoding="UTF-8"?>') assert xml.include?('<name>Matz</name>') assert xml.include?('<id type="integer">1</id>') @@ -1030,7 +1030,7 @@ class BaseTest < Test::Unit::TestCase encode = matz.encode xml = matz.to_xml - assert encode, xml + assert_equal encode, xml assert xml.include?('<?xml version="1.0" encoding="UTF-8"?>') assert xml.include?('<ruby-creator>') assert xml.include?('<name>Matz</name>') @@ -1065,7 +1065,7 @@ class BaseTest < Test::Unit::TestCase json = joe.to_json Person.format = :xml - assert encode, json + assert_equal encode, json assert_match %r{^\{"ruby_creator":\{"person":\{}, json assert_match %r{"id":6}, json assert_match %r{"name":"Joe"}, json @@ -1081,6 +1081,13 @@ class BaseTest < Test::Unit::TestCase assert_equal '1', matz.to_param end + def test_to_key_quacks_like_active_record + new_person = Person.new + assert_nil new_person.to_key + matz = Person.find(1) + assert_equal [1], matz.to_key + end + def test_parse_deep_nested_resources luis = Customer.find(1) assert_kind_of Customer, luis diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index c617a0ec3b..0a25ad0406 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,8 +1,10 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* Ruby 1.9: support UTF-8 case folding. #4595 [Norman Clarke] + * Renames Array#rand -> Array#random_element. [Santiago Pastorino, Rizwan Reza] -* 1.9 compat: Renames last_(month|year) to prev_(month|year) in Date and Time. [fxn] +* Ruby 1.9: Renames last_(month|year) to prev_(month|year) in Date and Time. [fxn] * Aliases Date#sunday to Date#end_of_week. [fxn] diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index f8e032139f..51edb59c77 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -11,126 +11,129 @@ require 'tmpdir' module ActiveSupport module Multibyte - class UnicodeDatabase - def load; end - end - - class UnicodeDatabaseGenerator - BASE_URI = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::UNICODE_VERSION}/ucd/" - SOURCES = { - :codepoints => BASE_URI + 'UnicodeData.txt', - :composition_exclusion => BASE_URI + 'CompositionExclusions.txt', - :grapheme_break_property => BASE_URI + 'auxiliary/GraphemeBreakProperty.txt', - :cp1252 => 'http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT' - } - - def initialize - @ucd = UnicodeDatabase.new - - default = Codepoint.new - default.combining_class = 0 - default.uppercase_mapping = 0 - default.lowercase_mapping = 0 - @ucd.codepoints = Hash.new(default) - end + module Unicode - def parse_codepoints(line) - codepoint = Codepoint.new - raise "Could not parse input." unless line =~ /^ - ([0-9A-F]+); # code - ([^;]+); # name - ([A-Z]+); # general category - ([0-9]+); # canonical combining class - ([A-Z]+); # bidi class - (<([A-Z]*)>)? # decomposition type - ((\ ?[0-9A-F]+)*); # decompomposition mapping - ([0-9]*); # decimal digit - ([0-9]*); # digit - ([^;]*); # numeric - ([YN]*); # bidi mirrored - ([^;]*); # unicode 1.0 name - ([^;]*); # iso comment - ([0-9A-F]*); # simple uppercase mapping - ([0-9A-F]*); # simple lowercase mapping - ([0-9A-F]*)$/ix # simple titlecase mapping - codepoint.code = $1.hex - #codepoint.name = $2 - #codepoint.category = $3 - codepoint.combining_class = Integer($4) - #codepoint.bidi_class = $5 - codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex } - #codepoint.bidi_mirrored = ($13=='Y') ? true : false - codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex - codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex - #codepoint.titlecase_mapping = ($18=='') ? nil : $18.hex - @ucd.codepoints[codepoint.code] = codepoint + class UnicodeDatabase + def load; end end - def parse_grapheme_break_property(line) - if line =~ /^([0-9A-F\.]+)\s*;\s*([\w]+)\s*#/ - type = $2.downcase.intern - @ucd.boundary[type] ||= [] - if $1.include? '..' - parts = $1.split '..' - @ucd.boundary[type] << (parts[0].hex..parts[1].hex) - else - @ucd.boundary[type] << $1.hex + class DatabaseGenerator + BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/" + SOURCES = { + :codepoints => BASE_URI + 'UnicodeData.txt', + :composition_exclusion => BASE_URI + 'CompositionExclusions.txt', + :grapheme_break_property => BASE_URI + 'auxiliary/GraphemeBreakProperty.txt', + :cp1252 => 'http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT' + } + + def initialize + @ucd = Unicode::UnicodeDatabase.new + + default = Codepoint.new + default.combining_class = 0 + default.uppercase_mapping = 0 + default.lowercase_mapping = 0 + @ucd.codepoints = Hash.new(default) + end + + def parse_codepoints(line) + codepoint = Codepoint.new + raise "Could not parse input." unless line =~ /^ + ([0-9A-F]+); # code + ([^;]+); # name + ([A-Z]+); # general category + ([0-9]+); # canonical combining class + ([A-Z]+); # bidi class + (<([A-Z]*)>)? # decomposition type + ((\ ?[0-9A-F]+)*); # decompomposition mapping + ([0-9]*); # decimal digit + ([0-9]*); # digit + ([^;]*); # numeric + ([YN]*); # bidi mirrored + ([^;]*); # unicode 1.0 name + ([^;]*); # iso comment + ([0-9A-F]*); # simple uppercase mapping + ([0-9A-F]*); # simple lowercase mapping + ([0-9A-F]*)$/ix # simple titlecase mapping + codepoint.code = $1.hex + #codepoint.name = $2 + #codepoint.category = $3 + codepoint.combining_class = Integer($4) + #codepoint.bidi_class = $5 + codepoint.decomp_type = $7 + codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex } + #codepoint.bidi_mirrored = ($13=='Y') ? true : false + codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex + codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex + #codepoint.titlecase_mapping = ($18=='') ? nil : $18.hex + @ucd.codepoints[codepoint.code] = codepoint + end + + def parse_grapheme_break_property(line) + if line =~ /^([0-9A-F\.]+)\s*;\s*([\w]+)\s*#/ + type = $2.downcase.intern + @ucd.boundary[type] ||= [] + if $1.include? '..' + parts = $1.split '..' + @ucd.boundary[type] << (parts[0].hex..parts[1].hex) + else + @ucd.boundary[type] << $1.hex + end end end - end - def parse_composition_exclusion(line) - if line =~ /^([0-9A-F]+)/i - @ucd.composition_exclusion << $1.hex + def parse_composition_exclusion(line) + if line =~ /^([0-9A-F]+)/i + @ucd.composition_exclusion << $1.hex + end end - end - def parse_cp1252(line) - if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i - @ucd.cp1252[$1.hex] = $2.hex + def parse_cp1252(line) + if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i + @ucd.cp1252[$1.hex] = $2.hex + end end - end - def create_composition_map - @ucd.codepoints.each do |_, cp| - if !cp.nil? and cp.combining_class == 0 and cp.decomp_type.nil? and !cp.decomp_mapping.nil? and cp.decomp_mapping.length == 2 and @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 and !@ucd.composition_exclusion.include?(cp.code) - @ucd.composition_map[cp.decomp_mapping[0]] ||= {} - @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code + def create_composition_map + @ucd.codepoints.each do |_, cp| + if !cp.nil? and cp.combining_class == 0 and cp.decomp_type.nil? and !cp.decomp_mapping.nil? and cp.decomp_mapping.length == 2 and @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 and !@ucd.composition_exclusion.include?(cp.code) + @ucd.composition_map[cp.decomp_mapping[0]] ||= {} + @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code + end end end - end - def normalize_boundary_map - @ucd.boundary.each do |k,v| - if [:lf, :cr].include? k - @ucd.boundary[k] = v[0] + def normalize_boundary_map + @ucd.boundary.each do |k,v| + if [:lf, :cr].include? k + @ucd.boundary[k] = v[0] + end end end - end - def parse - SOURCES.each do |type, url| - filename = File.join(Dir.tmpdir, "#{url.split('/').last}") - unless File.exist?(filename) - $stderr.puts "Downloading #{url.split('/').last}" - File.open(filename, 'wb') do |target| - open(url) do |source| - source.each_line { |line| target.write line } + def parse + SOURCES.each do |type, url| + filename = File.join(Dir.tmpdir, "#{url.split('/').last}") + unless File.exist?(filename) + $stderr.puts "Downloading #{url.split('/').last}" + File.open(filename, 'wb') do |target| + open(url) do |source| + source.each_line { |line| target.write line } + end end end + File.open(filename) do |file| + file.each_line { |line| send "parse_#{type}".intern, line } + end end - File.open(filename) do |file| - file.each_line { |line| send "parse_#{type}".intern, line } - end + create_composition_map + normalize_boundary_map end - create_composition_map - normalize_boundary_map - end - def dump_to(filename) - File.open(filename, 'wb') do |f| - f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252]) + def dump_to(filename) + File.open(filename, 'wb') do |f| + f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252]) + end end end end @@ -138,8 +141,8 @@ module ActiveSupport end if __FILE__ == $0 - filename = ActiveSupport::Multibyte::UnicodeDatabase.filename - generator = ActiveSupport::Multibyte::UnicodeDatabaseGenerator.new + filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename + generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new generator.parse print "Writing to: #{filename}" generator.dump_to filename diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 5a7b94ead7..933667c909 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -203,8 +203,8 @@ module ActiveSupport # end # name = "_conditional_callback_#{@kind}_#{next_id}" - txt, line = <<-RUBY_EVAL, __LINE__ + 1 - def #{name}(halted) + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{name}(halted) #{@compiled_options[0] || "if true"} && !halted #{@filter} do yield self @@ -214,7 +214,6 @@ module ActiveSupport end end RUBY_EVAL - @klass.class_eval(txt, __FILE__, line) "#{name}(halted) do" end end @@ -312,9 +311,9 @@ module ActiveSupport def _normalize_legacy_filter(kind, filter) if !filter.respond_to?(kind) && filter.respond_to?(:filter) - filter.singleton_class.class_eval( - "def #{kind}(context, &block) filter(context, &block) end", - __FILE__, __LINE__ - 1) + filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{kind}(context, &block) filter(context, &block) end + RUBY_EVAL elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around def filter.around(context) should_continue = before(context) @@ -387,31 +386,29 @@ module ActiveSupport send("_update_#{symbol}_superclass_callbacks") body = send("_#{symbol}_callbacks").compile(nil) - body, line = <<-RUBY_EVAL, __LINE__ + 1 - def _run_#{symbol}_callbacks(key = nil, &blk) - if self.class.send("_update_#{symbol}_superclass_callbacks") - self.class.__define_runner(#{symbol.inspect}) - return _run_#{symbol}_callbacks(key, &blk) - end + silence_warnings do + undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks") + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def _run_#{symbol}_callbacks(key = nil, &blk) + if self.class.send("_update_#{symbol}_superclass_callbacks") + self.class.__define_runner(#{symbol.inspect}) + return _run_#{symbol}_callbacks(key, &blk) + end - if key - name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks" + if key + name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks" - unless respond_to?(name) - self.class.__create_keyed_callback(name, :#{symbol}, self, &blk) - end + unless respond_to?(name) + self.class.__create_keyed_callback(name, :#{symbol}, self, &blk) + end - send(name, &blk) - else - #{body} + send(name, &blk) + else + #{body} + end end - end - private :_run_#{symbol}_callbacks - RUBY_EVAL - - silence_warnings do - undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks") - class_eval body, __FILE__, line + private :_run_#{symbol}_callbacks + RUBY_EVAL end end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index d2bcd7a778..576366e496 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -61,10 +61,7 @@ class Class end RUBY - if instance_writer - body = "def #{name}=(value) @#{name} = value end" - class_eval body, __FILE__, __LINE__ - 1 - end + attr_writer name if instance_writer end end end diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index d052bfed2d..28bc30ae26 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -2,14 +2,14 @@ class Module # Declares an attribute reader backed by an internally-named instance variable. def attr_internal_reader(*attrs) attrs.each do |attr| - module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end" + module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end", __FILE__, __LINE__ end end # Declares an attribute writer backed by an internally-named instance variable. def attr_internal_writer(*attrs) attrs.each do |attr| - module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end" + module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end", __FILE__, __LINE__ end end diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 42e053d0f8..3dfe996d06 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -2,7 +2,7 @@ require 'active_support/multibyte' class String - unless '1.9'.respond_to?(:force_encoding) + if '1.9'.respond_to?(:force_encoding) # == Multibyte proxy # # +mb_chars+ is a multibyte safe proxy for string methods. @@ -37,23 +37,13 @@ class String # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For # information about how to change the default Multibyte behaviour see ActiveSupport::Multibyte. def mb_chars - if ActiveSupport::Multibyte.proxy_class.wants?(self) + if ActiveSupport::Multibyte.proxy_class.consumes?(self) ActiveSupport::Multibyte.proxy_class.new(self) else self end end - - # Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have - # them), returns false otherwise. - def is_utf8? - ActiveSupport::Multibyte::Chars.consumes?(self) - end - else - def mb_chars #:nodoc - self - end - + def is_utf8? #:nodoc case encoding when Encoding::UTF_8 @@ -64,5 +54,19 @@ class String false end end + else + def mb_chars + if ActiveSupport::Multibyte.proxy_class.wants?(self) + ActiveSupport::Multibyte.proxy_class.new(self) + else + self + end + end + + # Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have + # them), returns false otherwise. + def is_utf8? + ActiveSupport::Multibyte::Chars.consumes?(self) + end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 9c4412c28c..e14e225596 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -66,7 +66,7 @@ module ActiveSupport #:nodoc: end def self.locked(*methods) - methods.each { |m| class_eval "def #{m}(*) lock { super } end" } + methods.each { |m| class_eval "def #{m}(*) lock { super } end", __FILE__, __LINE__ } end def get(key) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index db5afb5324..cd0d66a482 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -38,6 +38,7 @@ module ActiveSupport def is_a?(klass) #:nodoc: Duration == klass || value.is_a?(klass) end + alias :kind_of? :is_a? # Returns true if <tt>other</tt> is also a Duration instance with the # same <tt>value</tt>, or if <tt>other == value</tt>. diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 2344bb1bb3..bccc5425a6 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -58,8 +58,9 @@ module ActiveSupport # transliterate("Jürgen") # # => "Juergen" def transliterate(string, replacement = "?") - I18n.transliterate(Multibyte::Chars.normalize( - Multibyte::Chars.tidy_bytes(string), :c), :replacement => replacement) + I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( + ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), + :replacement => replacement) end # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb index 428c48a484..e7a271a660 100644 --- a/activesupport/lib/active_support/multibyte.rb +++ b/activesupport/lib/active_support/multibyte.rb @@ -1,30 +1,12 @@ # encoding: utf-8 - require 'active_support/core_ext/module/attribute_accessors' module ActiveSupport #:nodoc: module Multibyte autoload :EncodingError, 'active_support/multibyte/exceptions' autoload :Chars, 'active_support/multibyte/chars' - autoload :UnicodeDatabase, 'active_support/multibyte/unicode_database' - autoload :Codepoint, 'active_support/multibyte/unicode_database' - autoload :UCD, 'active_support/multibyte/unicode_database' + autoload :Unicode, 'active_support/multibyte/unicode' - # A list of all available normalization forms. See http://www.unicode.org/reports/tr15/tr15-29.html for more - # information about normalization. - NORMALIZATION_FORMS = [:c, :kc, :d, :kd] - - # The Unicode version that is supported by the implementation - UNICODE_VERSION = '5.1.0' - - # The default normalization used for operations that require normalization. It can be set to any of the - # normalizations in NORMALIZATION_FORMS. - # - # Example: - # ActiveSupport::Multibyte.default_normalization_form = :c - mattr_accessor :default_normalization_form - self.default_normalization_form = :kc - # The proxy class returned when calling mb_chars. You can use this accessor to configure your own proxy # class so you can support other encodings. See the ActiveSupport::Multibyte::Chars implementation for # an example how to do this. diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index cca30d1141..429b65bf15 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -34,52 +34,12 @@ module ActiveSupport #:nodoc: # # ActiveSupport::Multibyte.proxy_class = CharsForUTF32 class Chars - # Hangul character boundaries and properties - HANGUL_SBASE = 0xAC00 - HANGUL_LBASE = 0x1100 - HANGUL_VBASE = 0x1161 - HANGUL_TBASE = 0x11A7 - HANGUL_LCOUNT = 19 - HANGUL_VCOUNT = 21 - HANGUL_TCOUNT = 28 - HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT - HANGUL_SCOUNT = 11172 - HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT - HANGUL_JAMO_FIRST = 0x1100 - HANGUL_JAMO_LAST = 0x11FF - - # All the unicode whitespace - UNICODE_WHITESPACE = [ - (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D> - 0x0020, # White_Space # Zs SPACE - 0x0085, # White_Space # Cc <control-0085> - 0x00A0, # White_Space # Zs NO-BREAK SPACE - 0x1680, # White_Space # Zs OGHAM SPACE MARK - 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR - (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE - 0x2028, # White_Space # Zl LINE SEPARATOR - 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR - 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE - 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE - 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE - ].flatten.freeze - - # BOM (byte order mark) can also be seen as whitespace, it's a non-rendering character used to distinguish - # between little and big endian. This is not an issue in utf-8, so it must be ignored. - UNICODE_LEADERS_AND_TRAILERS = UNICODE_WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM - - # Returns a regular expression pattern that matches the passed Unicode codepoints - def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: - array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') - end - UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/u - UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/u attr_reader :wrapped_string alias to_s wrapped_string alias to_str wrapped_string - if '1.9'.respond_to?(:force_encoding) + if RUBY_VERSION >= "1.9" # Creates a new Chars instance by wrapping _string_. def initialize(string) @wrapped_string = string @@ -113,12 +73,6 @@ module ActiveSupport #:nodoc: true end - # Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns - # +false+ otherwise. - def self.wants?(string) - $KCODE == 'UTF8' && consumes?(string) - end - # Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise. def self.consumes?(string) # Unpack is a little bit faster than regular expressions. @@ -130,89 +84,131 @@ module ActiveSupport #:nodoc: include Comparable - # Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before, - # equal or after the object on the right side of the operation. It accepts any object that implements +to_s+. - # See <tt>String#<=></tt> for more details. - # - # Example: - # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 - def <=>(other) - @wrapped_string <=> other.to_s - end + if RUBY_VERSION < "1.9" + # Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns + # +false+ otherwise. + def self.wants?(string) + $KCODE == 'UTF8' && consumes?(string) + end - # Returns a new Chars object containing the _other_ object concatenated to the string. - # - # Example: - # ('Café'.mb_chars + ' périferôl').to_s #=> "Café périferôl" - def +(other) - self << other - end + # Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before, + # equal or after the object on the right side of the operation. It accepts any object that implements +to_s+. + # See <tt>String#<=></tt> for more details. + # + # Example: + # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 + def <=>(other) + @wrapped_string <=> other.to_s + end - # Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset. - # - # Example: - # 'Café périferôl'.mb_chars =~ /ô/ #=> 12 - def =~(other) - translate_offset(@wrapped_string =~ other) - end + # Returns a new Chars object containing the _other_ object concatenated to the string. + # + # Example: + # ('Café'.mb_chars + ' périferôl').to_s #=> "Café périferôl" + def +(other) + self << other + end - # Works just like <tt>String#split</tt>, with the exception that the items in the resulting list are Chars - # instances instead of String. This makes chaining methods easier. - # - # Example: - # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } #=> ["CAF", " P", "RIFERÔL"] - def split(*args) - @wrapped_string.split(*args).map { |i| i.mb_chars } - end + # Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset. + # + # Example: + # 'Café périferôl'.mb_chars =~ /ô/ #=> 12 + def =~(other) + translate_offset(@wrapped_string =~ other) + end - # Inserts the passed string at specified codepoint offsets. - # - # Example: - # 'Café'.mb_chars.insert(4, ' périferôl').to_s #=> "Café périferôl" - def insert(offset, fragment) - unpacked = self.class.u_unpack(@wrapped_string) - unless offset > unpacked.length - @wrapped_string.replace( - self.class.u_unpack(@wrapped_string).insert(offset, *self.class.u_unpack(fragment)).pack('U*') - ) - else - raise IndexError, "index #{offset} out of string" + # Inserts the passed string at specified codepoint offsets. + # + # Example: + # 'Café'.mb_chars.insert(4, ' périferôl').to_s #=> "Café périferôl" + def insert(offset, fragment) + unpacked = Unicode.u_unpack(@wrapped_string) + unless offset > unpacked.length + @wrapped_string.replace( + Unicode.u_unpack(@wrapped_string).insert(offset, *Unicode.u_unpack(fragment)).pack('U*') + ) + else + raise IndexError, "index #{offset} out of string" + end + self end - self - end - # Returns +true+ if contained string contains _other_. Returns +false+ otherwise. - # - # Example: - # 'Café'.mb_chars.include?('é') #=> true - def include?(other) - # We have to redefine this method because Enumerable defines it. - @wrapped_string.include?(other) - end + # Returns +true+ if contained string contains _other_. Returns +false+ otherwise. + # + # Example: + # 'Café'.mb_chars.include?('é') #=> true + def include?(other) + # We have to redefine this method because Enumerable defines it. + @wrapped_string.include?(other) + end - # Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found. - # - # Example: - # 'Café périferôl'.mb_chars.index('ô') #=> 12 - # 'Café périferôl'.mb_chars.index(/\w/u) #=> 0 - def index(needle, offset=0) - wrapped_offset = first(offset).wrapped_string.length - index = @wrapped_string.index(needle, wrapped_offset) - index ? (self.class.u_unpack(@wrapped_string.slice(0...index)).size) : nil + # Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found. + # + # Example: + # 'Café périferôl'.mb_chars.index('ô') #=> 12 + # 'Café périferôl'.mb_chars.index(/\w/u) #=> 0 + def index(needle, offset=0) + wrapped_offset = first(offset).wrapped_string.length + index = @wrapped_string.index(needle, wrapped_offset) + index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil + end + + # Returns the position _needle_ in the string, counting in + # codepoints, searching backward from _offset_ or the end of the + # string. Returns +nil+ if _needle_ isn't found. + # + # Example: + # 'Café périferôl'.mb_chars.rindex('é') #=> 6 + # 'Café périferôl'.mb_chars.rindex(/\w/u) #=> 13 + def rindex(needle, offset=nil) + offset ||= length + wrapped_offset = first(offset).wrapped_string.length + index = @wrapped_string.rindex(needle, wrapped_offset) + index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil + end + + # Returns the number of codepoints in the string + def size + Unicode.u_unpack(@wrapped_string).size + end + alias_method :length, :size + + # Strips entire range of Unicode whitespace from the right of the string. + def rstrip + chars(@wrapped_string.gsub(Unicode::TRAILERS_PAT, '')) + end + + # Strips entire range of Unicode whitespace from the left of the string. + def lstrip + chars(@wrapped_string.gsub(Unicode::LEADERS_PAT, '')) + end + + # Strips entire range of Unicode whitespace from the right and left of the string. + def strip + rstrip.lstrip + end + + # Returns the codepoint of the first character in the string. + # + # Example: + # 'こんにちは'.mb_chars.ord #=> 12371 + def ord + Unicode.u_unpack(@wrapped_string)[0] + end + + else + def =~(other) + @wrapped_string =~ other + end end - # Returns the position _needle_ in the string, counting in - # codepoints, searching backward from _offset_ or the end of the - # string. Returns +nil+ if _needle_ isn't found. + # Works just like <tt>String#split</tt>, with the exception that the items in the resulting list are Chars + # instances instead of String. This makes chaining methods easier. # # Example: - # 'Café périferôl'.mb_chars.rindex('é') #=> 6 - # 'Café périferôl'.mb_chars.rindex(/\w/u) #=> 13 - def rindex(needle, offset=nil) - offset ||= length - wrapped_offset = first(offset).wrapped_string.length - index = @wrapped_string.rindex(needle, wrapped_offset) - index ? (self.class.u_unpack(@wrapped_string.slice(0...index)).size) : nil + # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } #=> ["CAF", " P", "RIFERÔL"] + def split(*args) + @wrapped_string.split(*args).map { |i| i.mb_chars } end # Like <tt>String#[]=</tt>, except instead of byte offsets you specify character offsets. @@ -234,7 +230,7 @@ module ActiveSupport #:nodoc: if args.first.is_a?(Regexp) @wrapped_string[*args] = replace_by else - result = self.class.u_unpack(@wrapped_string) + result = Unicode.u_unpack(@wrapped_string) if args[0].is_a?(Fixnum) raise IndexError, "index #{args[0]} out of string" if args[0] >= result.length min = args[0] @@ -247,10 +243,10 @@ module ActiveSupport #:nodoc: else needle = args[0].to_s min = index(needle) - max = min + self.class.u_unpack(needle).length - 1 + max = min + Unicode.u_unpack(needle).length - 1 range = Range.new(min, max) end - result[range] = self.class.u_unpack(replace_by) + result[range] = Unicode.u_unpack(replace_by) @wrapped_string.replace(result.pack('U*')) end end @@ -294,33 +290,13 @@ module ActiveSupport #:nodoc: justify(integer, :center, padstr) end - # Strips entire range of Unicode whitespace from the right of the string. - def rstrip - chars(@wrapped_string.gsub(UNICODE_TRAILERS_PAT, '')) - end - - # Strips entire range of Unicode whitespace from the left of the string. - def lstrip - chars(@wrapped_string.gsub(UNICODE_LEADERS_PAT, '')) - end - - # Strips entire range of Unicode whitespace from the right and left of the string. - def strip - rstrip.lstrip - end - - # Returns the number of codepoints in the string - def size - self.class.u_unpack(@wrapped_string).size - end - alias_method :length, :size # Reverses all characters in the string. # # Example: # 'Café'.mb_chars.reverse.to_s #=> 'éfaC' def reverse - chars(self.class.g_unpack(@wrapped_string).reverse.flatten.pack('U*')) + chars(Unicode.g_unpack(@wrapped_string).reverse.flatten.pack('U*')) end # Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that @@ -336,15 +312,15 @@ module ActiveSupport #:nodoc: elsif (args.size == 2 && !args[1].is_a?(Numeric)) raise TypeError, "cannot convert #{args[1].class} into Integer" # Do as if we were native elsif args[0].kind_of? Range - cps = self.class.u_unpack(@wrapped_string).slice(*args) + cps = Unicode.u_unpack(@wrapped_string).slice(*args) result = cps.nil? ? nil : cps.pack('U*') elsif args[0].kind_of? Regexp result = @wrapped_string.slice(*args) elsif args.size == 1 && args[0].kind_of?(Numeric) - character = self.class.u_unpack(@wrapped_string)[args[0]] + character = Unicode.u_unpack(@wrapped_string)[args[0]] result = character.nil? ? nil : [character].pack('U') else - result = self.class.u_unpack(@wrapped_string).slice(*args).pack('U*') + result = Unicode.u_unpack(@wrapped_string).slice(*args).pack('U*') end result.nil? ? nil : chars(result) end @@ -372,20 +348,12 @@ module ActiveSupport #:nodoc: slice(0...translate_offset(limit)) end - # Returns the codepoint of the first character in the string. - # - # Example: - # 'こんにちは'.mb_chars.ord #=> 12371 - def ord - self.class.u_unpack(@wrapped_string)[0] - end - # Convert characters in the string to uppercase. # # Example: # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s #=> "LAURENT, OÙ SONT LES TESTS ?" def upcase - apply_mapping :uppercase_mapping + chars(Unicode.apply_mapping @wrapped_string, :uppercase_mapping) end # Convert characters in the string to lowercase. @@ -393,7 +361,7 @@ module ActiveSupport #:nodoc: # Example: # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s #=> "věda a výzkum" def downcase - apply_mapping :lowercase_mapping + chars(Unicode.apply_mapping @wrapped_string, :lowercase_mapping) end # Converts the first character to uppercase and the remainder to lowercase. @@ -409,9 +377,9 @@ module ActiveSupport #:nodoc: # # * <tt>form</tt> - The form you want to normalize in. Should be one of the following: # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is - # ActiveSupport::Multibyte.default_normalization_form - def normalize(form=ActiveSupport::Multibyte.default_normalization_form) - chars(self.class.normalize(@wrapped_string, form)) + # ActiveSupport::Multibyte::Unicode.default_normalization_form + def normalize(form = nil) + chars(Unicode.normalize(@wrapped_string, form)) end # Performs canonical decomposition on all the characters. @@ -420,7 +388,7 @@ module ActiveSupport #:nodoc: # 'é'.length #=> 2 # 'é'.mb_chars.decompose.to_s.length #=> 3 def decompose - chars(self.class.decompose_codepoints(:canonical, self.class.u_unpack(@wrapped_string)).pack('U*')) + chars(Unicode.decompose_codepoints(:canonical, Unicode.u_unpack(@wrapped_string)).pack('U*')) end # Performs composition on all the characters. @@ -429,7 +397,7 @@ module ActiveSupport #:nodoc: # 'é'.length #=> 3 # 'é'.mb_chars.compose.to_s.length #=> 2 def compose - chars(self.class.compose_codepoints(self.class.u_unpack(@wrapped_string)).pack('U*')) + chars(Unicode.compose_codepoints(Unicode.u_unpack(@wrapped_string)).pack('U*')) end # Returns the number of grapheme clusters in the string. @@ -438,14 +406,14 @@ module ActiveSupport #:nodoc: # 'क्षि'.mb_chars.length #=> 4 # 'क्षि'.mb_chars.g_length #=> 3 def g_length - self.class.g_unpack(@wrapped_string).length + Unicode.g_unpack(@wrapped_string).length end # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. # # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1. def tidy_bytes(force = false) - chars(self.class.tidy_bytes(@wrapped_string, force)) + chars(Unicode.tidy_bytes(@wrapped_string, force)) end %w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method| @@ -459,266 +427,6 @@ module ActiveSupport #:nodoc: end end - class << self - - # Unpack the string at codepoints boundaries. Raises an EncodingError when the encoding of the string isn't - # valid UTF-8. - # - # Example: - # Chars.u_unpack('Café') #=> [67, 97, 102, 233] - def u_unpack(string) - begin - string.unpack 'U*' - rescue ArgumentError - raise EncodingError, 'malformed UTF-8 character' - end - end - - # Detect whether the codepoint is in a certain character class. Returns +true+ when it's in the specified - # character class and +false+ otherwise. Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>, - # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>. - # - # Primarily used by the grapheme cluster support. - def in_char_class?(codepoint, classes) - classes.detect { |c| UCD.boundary[c] === codepoint } ? true : false - end - - # Unpack the string at grapheme boundaries. Returns a list of character lists. - # - # Example: - # Chars.g_unpack('क्षि') #=> [[2325, 2381], [2359], [2367]] - # Chars.g_unpack('Café') #=> [[67], [97], [102], [233]] - def g_unpack(string) - codepoints = u_unpack(string) - unpacked = [] - pos = 0 - marker = 0 - eoc = codepoints.length - while(pos < eoc) - pos += 1 - previous = codepoints[pos-1] - current = codepoints[pos] - if ( - # CR X LF - one = ( previous == UCD.boundary[:cr] and current == UCD.boundary[:lf] ) or - # L X (L|V|LV|LVT) - two = ( UCD.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or - # (LV|V) X (V|T) - three = ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or - # (LVT|T) X (T) - four = ( in_char_class?(previous, [:lvt,:t]) and UCD.boundary[:t] === current ) or - # X Extend - five = (UCD.boundary[:extend] === current) - ) - else - unpacked << codepoints[marker..pos-1] - marker = pos - end - end - unpacked - end - - # Reverse operation of g_unpack. - # - # Example: - # Chars.g_pack(Chars.g_unpack('क्षि')) #=> 'क्षि' - def g_pack(unpacked) - (unpacked.flatten).pack('U*') - end - - def padding(padsize, padstr=' ') #:nodoc: - if padsize != 0 - new(padstr * ((padsize / u_unpack(padstr).size) + 1)).slice(0, padsize) - else - '' - end - end - - # Re-order codepoints so the string becomes canonical. - def reorder_characters(codepoints) - length = codepoints.length- 1 - pos = 0 - while pos < length do - cp1, cp2 = UCD.codepoints[codepoints[pos]], UCD.codepoints[codepoints[pos+1]] - if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0) - codepoints[pos..pos+1] = cp2.code, cp1.code - pos += (pos > 0 ? -1 : 1) - else - pos += 1 - end - end - codepoints - end - - # Decompose composed characters to the decomposed form. - def decompose_codepoints(type, codepoints) - codepoints.inject([]) do |decomposed, cp| - # if it's a hangul syllable starter character - if HANGUL_SBASE <= cp and cp < HANGUL_SLAST - sindex = cp - HANGUL_SBASE - ncp = [] # new codepoints - ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT - ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT - tindex = sindex % HANGUL_TCOUNT - ncp << (HANGUL_TBASE + tindex) unless tindex == 0 - decomposed.concat ncp - # if the codepoint is decomposable in with the current decomposition type - elsif (ncp = UCD.codepoints[cp].decomp_mapping) and (!UCD.codepoints[cp].decomp_type || type == :compatability) - decomposed.concat decompose_codepoints(type, ncp.dup) - else - decomposed << cp - end - end - end - - # Compose decomposed characters to the composed form. - def compose_codepoints(codepoints) - pos = 0 - eoa = codepoints.length - 1 - starter_pos = 0 - starter_char = codepoints[0] - previous_combining_class = -1 - while pos < eoa - pos += 1 - lindex = starter_char - HANGUL_LBASE - # -- Hangul - if 0 <= lindex and lindex < HANGUL_LCOUNT - vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1 - if 0 <= vindex and vindex < HANGUL_VCOUNT - tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1 - if 0 <= tindex and tindex < HANGUL_TCOUNT - j = starter_pos + 2 - eoa -= 2 - else - tindex = 0 - j = starter_pos + 1 - eoa -= 1 - end - codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE - end - starter_pos += 1 - starter_char = codepoints[starter_pos] - # -- Other characters - else - current_char = codepoints[pos] - current = UCD.codepoints[current_char] - if current.combining_class > previous_combining_class - if ref = UCD.composition_map[starter_char] - composition = ref[current_char] - else - composition = nil - end - unless composition.nil? - codepoints[starter_pos] = composition - starter_char = composition - codepoints.delete_at pos - eoa -= 1 - pos -= 1 - previous_combining_class = -1 - else - previous_combining_class = current.combining_class - end - else - previous_combining_class = current.combining_class - end - if current.combining_class == 0 - starter_pos = pos - starter_char = codepoints[pos] - end - end - end - codepoints - end - - def tidy_byte(byte) - if byte < 160 - [UCD.cp1252[byte] || byte].pack("U").unpack("C*") - elsif byte < 192 - [194, byte] - else - [195, byte - 64] - end - end - private :tidy_byte - - # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. - # - # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1. - def tidy_bytes(string, force = false) - if force - return string.unpack("C*").map do |b| - tidy_byte(b) - end.flatten.compact.pack("C*").unpack("U*").pack("U*") - end - - bytes = string.unpack("C*") - conts_expected = 0 - last_lead = 0 - - bytes.each_index do |i| - - byte = bytes[i] - is_ascii = byte < 128 - is_cont = byte > 127 && byte < 192 - is_lead = byte > 191 && byte < 245 - is_unused = byte > 240 - is_restricted = byte > 244 - - # Impossible or highly unlikely byte? Clean it. - if is_unused || is_restricted - bytes[i] = tidy_byte(byte) - elsif is_cont - # Not expecting contination byte? Clean up. Otherwise, now expect one less. - conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1 - else - if conts_expected > 0 - # Expected continuation, but got ASCII or leading? Clean backwards up to - # the leading byte. - (1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])} - conts_expected = 0 - end - if is_lead - # Final byte is leading? Clean it. - if i == bytes.length - 1 - bytes[i] = tidy_byte(bytes.last) - else - # Valid leading byte? Expect continuations determined by position of - # first zero bit, with max of 3. - conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3 - last_lead = i - end - end - end - end - bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*") - end - - # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for - # passing strings to databases and validations. - # - # * <tt>string</tt> - The string to perform normalization on. - # * <tt>form</tt> - The form you want to normalize in. Should be one of the following: - # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is - # ActiveSupport::Multibyte.default_normalization_form - def normalize(string, form=ActiveSupport::Multibyte.default_normalization_form) - # See http://www.unicode.org/reports/tr15, Table 1 - codepoints = u_unpack(string) - case form - when :d - reorder_characters(decompose_codepoints(:canonical, codepoints)) - when :c - compose_codepoints(reorder_characters(decompose_codepoints(:canonical, codepoints))) - when :kd - reorder_characters(decompose_codepoints(:compatability, codepoints)) - when :kc - compose_codepoints(reorder_characters(decompose_codepoints(:compatability, codepoints))) - else - raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack('U*') - end - - end - protected def translate_offset(byte_offset) #:nodoc: @@ -743,26 +451,23 @@ module ActiveSupport #:nodoc: padsize = padsize > 0 ? padsize : 0 case way when :right - result = @wrapped_string.dup.insert(0, self.class.padding(padsize, padstr)) + result = @wrapped_string.dup.insert(0, padding(padsize, padstr)) when :left - result = @wrapped_string.dup.insert(-1, self.class.padding(padsize, padstr)) + result = @wrapped_string.dup.insert(-1, padding(padsize, padstr)) when :center - lpad = self.class.padding((padsize / 2.0).floor, padstr) - rpad = self.class.padding((padsize / 2.0).ceil, padstr) + lpad = padding((padsize / 2.0).floor, padstr) + rpad = padding((padsize / 2.0).ceil, padstr) result = @wrapped_string.dup.insert(0, lpad).insert(-1, rpad) end chars(result) end - def apply_mapping(mapping) #:nodoc: - chars(self.class.u_unpack(@wrapped_string).map do |codepoint| - cp = UCD.codepoints[codepoint] - if cp and (ncp = cp.send(mapping)) and ncp > 0 - ncp - else - codepoint - end - end.pack('U*')) + def padding(padsize, padstr=' ') #:nodoc: + if padsize != 0 + chars(padstr * ((padsize / Unicode.u_unpack(padstr).size) + 1)).slice(0, padsize) + else + '' + end end def chars(string) #:nodoc: diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb new file mode 100644 index 0000000000..f91e50c755 --- /dev/null +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -0,0 +1,393 @@ +module ActiveSupport + module Multibyte + module Unicode + + extend self + + # A list of all available normalization forms. See http://www.unicode.org/reports/tr15/tr15-29.html for more + # information about normalization. + NORMALIZATION_FORMS = [:c, :kc, :d, :kd] + + # The Unicode version that is supported by the implementation + UNICODE_VERSION = '5.1.0' + + # The default normalization used for operations that require normalization. It can be set to any of the + # normalizations in NORMALIZATION_FORMS. + # + # Example: + # ActiveSupport::Multibyte::Unicode.default_normalization_form = :c + attr_accessor :default_normalization_form + @default_normalization_form = :kc + + # Hangul character boundaries and properties + HANGUL_SBASE = 0xAC00 + HANGUL_LBASE = 0x1100 + HANGUL_VBASE = 0x1161 + HANGUL_TBASE = 0x11A7 + HANGUL_LCOUNT = 19 + HANGUL_VCOUNT = 21 + HANGUL_TCOUNT = 28 + HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT + HANGUL_SCOUNT = 11172 + HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT + HANGUL_JAMO_FIRST = 0x1100 + HANGUL_JAMO_LAST = 0x11FF + + # All the unicode whitespace + WHITESPACE = [ + (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D> + 0x0020, # White_Space # Zs SPACE + 0x0085, # White_Space # Cc <control-0085> + 0x00A0, # White_Space # Zs NO-BREAK SPACE + 0x1680, # White_Space # Zs OGHAM SPACE MARK + 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR + (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE + 0x2028, # White_Space # Zl LINE SEPARATOR + 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR + 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE + 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE + 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE + ].flatten.freeze + + # BOM (byte order mark) can also be seen as whitespace, it's a non-rendering character used to distinguish + # between little and big endian. This is not an issue in utf-8, so it must be ignored. + LEADERS_AND_TRAILERS = WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM + + # Returns a regular expression pattern that matches the passed Unicode codepoints + def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: + array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') + end + TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u + LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u + + # Unpack the string at codepoints boundaries. Raises an EncodingError when the encoding of the string isn't + # valid UTF-8. + # + # Example: + # Unicode.u_unpack('Café') #=> [67, 97, 102, 233] + def u_unpack(string) + begin + string.unpack 'U*' + rescue ArgumentError + raise EncodingError, 'malformed UTF-8 character' + end + end + + # Detect whether the codepoint is in a certain character class. Returns +true+ when it's in the specified + # character class and +false+ otherwise. Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>, + # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>. + # + # Primarily used by the grapheme cluster support. + def in_char_class?(codepoint, classes) + classes.detect { |c| database.boundary[c] === codepoint } ? true : false + end + + # Unpack the string at grapheme boundaries. Returns a list of character lists. + # + # Example: + # Unicode.g_unpack('क्षि') #=> [[2325, 2381], [2359], [2367]] + # Unicode.g_unpack('Café') #=> [[67], [97], [102], [233]] + def g_unpack(string) + codepoints = u_unpack(string) + unpacked = [] + pos = 0 + marker = 0 + eoc = codepoints.length + while(pos < eoc) + pos += 1 + previous = codepoints[pos-1] + current = codepoints[pos] + if ( + # CR X LF + one = ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or + # L X (L|V|LV|LVT) + two = ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or + # (LV|V) X (V|T) + three = ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or + # (LVT|T) X (T) + four = ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or + # X Extend + five = (database.boundary[:extend] === current) + ) + else + unpacked << codepoints[marker..pos-1] + marker = pos + end + end + unpacked + end + + # Reverse operation of g_unpack. + # + # Example: + # Unicode.g_pack(Unicode.g_unpack('क्षि')) #=> 'क्षि' + def g_pack(unpacked) + (unpacked.flatten).pack('U*') + end + + # Re-order codepoints so the string becomes canonical. + def reorder_characters(codepoints) + length = codepoints.length- 1 + pos = 0 + while pos < length do + cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos+1]] + if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0) + codepoints[pos..pos+1] = cp2.code, cp1.code + pos += (pos > 0 ? -1 : 1) + else + pos += 1 + end + end + codepoints + end + + # Decompose composed characters to the decomposed form. + def decompose_codepoints(type, codepoints) + codepoints.inject([]) do |decomposed, cp| + # if it's a hangul syllable starter character + if HANGUL_SBASE <= cp and cp < HANGUL_SLAST + sindex = cp - HANGUL_SBASE + ncp = [] # new codepoints + ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT + ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT + tindex = sindex % HANGUL_TCOUNT + ncp << (HANGUL_TBASE + tindex) unless tindex == 0 + decomposed.concat ncp + # if the codepoint is decomposable in with the current decomposition type + elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatability) + decomposed.concat decompose_codepoints(type, ncp.dup) + else + decomposed << cp + end + end + end + + # Compose decomposed characters to the composed form. + def compose_codepoints(codepoints) + pos = 0 + eoa = codepoints.length - 1 + starter_pos = 0 + starter_char = codepoints[0] + previous_combining_class = -1 + while pos < eoa + pos += 1 + lindex = starter_char - HANGUL_LBASE + # -- Hangul + if 0 <= lindex and lindex < HANGUL_LCOUNT + vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1 + if 0 <= vindex and vindex < HANGUL_VCOUNT + tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1 + if 0 <= tindex and tindex < HANGUL_TCOUNT + j = starter_pos + 2 + eoa -= 2 + else + tindex = 0 + j = starter_pos + 1 + eoa -= 1 + end + codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE + end + starter_pos += 1 + starter_char = codepoints[starter_pos] + # -- Other characters + else + current_char = codepoints[pos] + current = database.codepoints[current_char] + if current.combining_class > previous_combining_class + if ref = database.composition_map[starter_char] + composition = ref[current_char] + else + composition = nil + end + unless composition.nil? + codepoints[starter_pos] = composition + starter_char = composition + codepoints.delete_at pos + eoa -= 1 + pos -= 1 + previous_combining_class = -1 + else + previous_combining_class = current.combining_class + end + else + previous_combining_class = current.combining_class + end + if current.combining_class == 0 + starter_pos = pos + starter_char = codepoints[pos] + end + end + end + codepoints + end + + # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. + # + # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1. + def tidy_bytes(string, force = false) + if force + return string.unpack("C*").map do |b| + tidy_byte(b) + end.flatten.compact.pack("C*").unpack("U*").pack("U*") + end + + bytes = string.unpack("C*") + conts_expected = 0 + last_lead = 0 + + bytes.each_index do |i| + + byte = bytes[i] + is_ascii = byte < 128 + is_cont = byte > 127 && byte < 192 + is_lead = byte > 191 && byte < 245 + is_unused = byte > 240 + is_restricted = byte > 244 + + # Impossible or highly unlikely byte? Clean it. + if is_unused || is_restricted + bytes[i] = tidy_byte(byte) + elsif is_cont + # Not expecting contination byte? Clean up. Otherwise, now expect one less. + conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1 + else + if conts_expected > 0 + # Expected continuation, but got ASCII or leading? Clean backwards up to + # the leading byte. + (1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])} + conts_expected = 0 + end + if is_lead + # Final byte is leading? Clean it. + if i == bytes.length - 1 + bytes[i] = tidy_byte(bytes.last) + else + # Valid leading byte? Expect continuations determined by position of + # first zero bit, with max of 3. + conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3 + last_lead = i + end + end + end + end + bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*") + end + + # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for + # passing strings to databases and validations. + # + # * <tt>string</tt> - The string to perform normalization on. + # * <tt>form</tt> - The form you want to normalize in. Should be one of the following: + # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is + # ActiveSupport::Multibyte.default_normalization_form + def normalize(string, form=nil) + form ||= @default_normalization_form + # See http://www.unicode.org/reports/tr15, Table 1 + codepoints = u_unpack(string) + case form + when :d + reorder_characters(decompose_codepoints(:canonical, codepoints)) + when :c + compose_codepoints(reorder_characters(decompose_codepoints(:canonical, codepoints))) + when :kd + reorder_characters(decompose_codepoints(:compatability, codepoints)) + when :kc + compose_codepoints(reorder_characters(decompose_codepoints(:compatability, codepoints))) + else + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end.pack('U*') + end + + def apply_mapping(string, mapping) #:nodoc: + u_unpack(string).map do |codepoint| + cp = database.codepoints[codepoint] + if cp and (ncp = cp.send(mapping)) and ncp > 0 + ncp + else + codepoint + end + end.pack('U*') + end + + # Holds data about a codepoint in the Unicode database + class Codepoint + attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping + end + + # Holds static data from the Unicode database + class UnicodeDatabase + ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 + + attr_writer(*ATTRIBUTES) + + def initialize + @codepoints = Hash.new(Codepoint.new) + @composition_exclusion = [] + @composition_map = {} + @boundary = {} + @cp1252 = {} + end + + # Lazy load the Unicode database so it's only loaded when it's actually used + ATTRIBUTES.each do |attr_name| + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{attr_name} # def codepoints + load # load + @#{attr_name} # @codepoints + end # end + EOS + end + + # Loads the Unicode database and returns all the internal objects of UnicodeDatabase. + def load + begin + @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read } + rescue Exception => e + raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") + end + + # Redefine the === method so we can write shorter rules for grapheme cluster breaks + @boundary.each do |k,_| + @boundary[k].instance_eval do + def ===(other) + detect { |i| i === other } ? true : false + end + end if @boundary[k].kind_of?(Array) + end + + # define attr_reader methods for the instance variables + class << self + attr_reader(*ATTRIBUTES) + end + end + + # Returns the directory in which the data files are stored + def self.dirname + File.dirname(__FILE__) + '/../values/' + end + + # Returns the filename for the data file for this version + def self.filename + File.expand_path File.join(dirname, "unicode_tables.dat") + end + end + + private + + def tidy_byte(byte) + if byte < 160 + [database.cp1252[byte] || byte].pack("U").unpack("C*") + elsif byte < 192 + [194, byte] + else + [195, byte - 64] + end + end + + def database + @database ||= UnicodeDatabase.new + end + + end + end +end diff --git a/activesupport/lib/active_support/multibyte/unicode_database.rb b/activesupport/lib/active_support/multibyte/unicode_database.rb deleted file mode 100644 index 074ad8613a..0000000000 --- a/activesupport/lib/active_support/multibyte/unicode_database.rb +++ /dev/null @@ -1,71 +0,0 @@ -# encoding: utf-8 - -module ActiveSupport #:nodoc: - module Multibyte #:nodoc: - # Holds data about a codepoint in the Unicode database - class Codepoint - attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping - end - - # Holds static data from the Unicode database - class UnicodeDatabase - ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 - - attr_writer(*ATTRIBUTES) - - def initialize - @codepoints = Hash.new(Codepoint.new) - @composition_exclusion = [] - @composition_map = {} - @boundary = {} - @cp1252 = {} - end - - # Lazy load the Unicode database so it's only loaded when it's actually used - ATTRIBUTES.each do |attr_name| - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{attr_name} # def codepoints - load # load - @#{attr_name} # @codepoints - end # end - EOS - end - - # Loads the Unicode database and returns all the internal objects of UnicodeDatabase. - def load - begin - @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read } - rescue Exception => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") - end - - # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each do |k,_| - @boundary[k].instance_eval do - def ===(other) - detect { |i| i === other } ? true : false - end - end if @boundary[k].kind_of?(Array) - end - - # define attr_reader methods for the instance variables - class << self - attr_reader(*ATTRIBUTES) - end - end - - # Returns the directory in which the data files are stored - def self.dirname - File.dirname(__FILE__) + '/../values/' - end - - # Returns the filename for the data file for this version - def self.filename - File.expand_path File.join(dirname, "unicode_tables.dat") - end - end - - # UniCode Database - UCD = UnicodeDatabase.new - end -end
\ No newline at end of file diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 0243157e35..59f9ab18b1 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -48,19 +48,20 @@ module I18n # Set the i18n configuration from config.i18n but special-case for # the load_path which should be appended to what's already set instead of overwritten. config.after_initialize do |app| + fallbacks = app.config.i18n.delete(:fallbacks) + app.config.i18n.each do |setting, value| case setting when :railties_load_path app.config.i18n.load_path.unshift(*value) when :load_path I18n.load_path += value - when :fallbacks - init_fallbacks(value) if value && validate_fallbacks(value) else I18n.send("#{setting}=", value) end end + init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) I18n.reload! end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index a7df473644..70a6c2ca60 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -7,7 +7,7 @@ module ActiveSupport unless method_defined?(:describe) def self.describe(text) - class_eval <<-RUBY_EVAL + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def self.name "#{text}" end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 74b333d416..10f2cae465 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 05f529dc7d..710c221fac 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -5,6 +5,7 @@ class DurationTest < ActiveSupport::TestCase def test_is_a d = 1.day assert d.is_a?(ActiveSupport::Duration) + assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d assert_kind_of Fixnum, d assert !d.is_a?(Hash) diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index ea21e445e2..759b0ddcd6 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -245,11 +245,9 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars end end - end - - if RUBY_VERSION >= '1.9' - def test_mb_chars_returns_string - assert_kind_of String, UNICODE_STRING.mb_chars + else + def test_mb_chars_returns_instance_of_proxy_class + assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars end end end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 77b1893f77..2cf5bd6ea9 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -326,7 +326,7 @@ class TimeWithZoneTest < Test::Unit::TestCase def test_is_a assert_kind_of Time, @twz assert_kind_of Time, @twz - assert ActiveSupport::TimeWithZone, @twz + assert_kind_of ActiveSupport::TimeWithZone, @twz end def test_class_name diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 2cbf9e5042..75ff88505d 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -371,7 +371,7 @@ class DependenciesTest < Test::Unit::TestCase end def test_custom_const_missing_should_work - Object.module_eval <<-end_eval + Object.module_eval <<-end_eval, __FILE__, __LINE__ + 1 module ModuleWithCustomConstMissing def self.const_missing(name) const_set name, name.to_s.hash diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 76bdc0e973..2990177bed 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -209,14 +209,14 @@ class InflectorTest < Test::Unit::TestCase end %w{plurals singulars uncountables humans}.each do |inflection_type| - class_eval " + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def test_clear_#{inflection_type} cached_values = ActiveSupport::Inflector.inflections.#{inflection_type} ActiveSupport::Inflector.inflections.clear :#{inflection_type} assert ActiveSupport::Inflector.inflections.#{inflection_type}.empty?, \"#{inflection_type} inflections should be empty after clear :#{inflection_type}\" ActiveSupport::Inflector.inflections.instance_variable_set :@#{inflection_type}, cached_values end - " + RUBY end def test_clear_all diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index f15b1351f7..f7a5834527 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -36,7 +36,7 @@ class MultibyteCharsTest < Test::Unit::TestCase end def test_forwarded_bang_method_calls_should_return_the_original_chars_instance - assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing! + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing! assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id end @@ -65,33 +65,32 @@ class MultibyteCharsTest < Test::Unit::TestCase end def test_unpack_utf8_strings - assert_equal 4, @proxy_class.u_unpack(UNICODE_STRING).length - assert_equal 5, @proxy_class.u_unpack(ASCII_STRING).length + assert_equal 4, ActiveSupport::Multibyte::Unicode.u_unpack(UNICODE_STRING).length + assert_equal 5, ActiveSupport::Multibyte::Unicode.u_unpack(ASCII_STRING).length end def test_unpack_raises_encoding_error_on_broken_strings assert_raise(ActiveSupport::Multibyte::EncodingError) do - @proxy_class.u_unpack(BYTE_STRING) + ActiveSupport::Multibyte::Unicode.u_unpack(BYTE_STRING) end end - if RUBY_VERSION < '1.9' - def test_concatenation_should_return_a_proxy_class_instance - assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars + 'b').class - assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars << 'b').class - end + def test_concatenation_should_return_a_proxy_class_instance + assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars + 'b').class + assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars << 'b').class + end - def test_ascii_strings_are_treated_at_utf8_strings - assert_equal ActiveSupport::Multibyte.proxy_class, ASCII_STRING.mb_chars.class - end + def test_ascii_strings_are_treated_at_utf8_strings + assert_equal ActiveSupport::Multibyte.proxy_class, ASCII_STRING.mb_chars.class + end - def test_concatenate_should_return_proxy_instance - assert(('a'.mb_chars + 'b').kind_of?(@proxy_class)) - assert(('a'.mb_chars + 'b'.mb_chars).kind_of?(@proxy_class)) - assert(('a'.mb_chars << 'b').kind_of?(@proxy_class)) - assert(('a'.mb_chars << 'b'.mb_chars).kind_of?(@proxy_class)) - end + def test_concatenate_should_return_proxy_instance + assert(('a'.mb_chars + 'b').kind_of?(@proxy_class)) + assert(('a'.mb_chars + 'b'.mb_chars).kind_of?(@proxy_class)) + assert(('a'.mb_chars << 'b').kind_of?(@proxy_class)) + assert(('a'.mb_chars << 'b'.mb_chars).kind_of?(@proxy_class)) end + end class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase @@ -111,35 +110,33 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase @byte_order_mark = [65279].pack('U') end - if RUBY_VERSION < '1.9' - def test_split_should_return_an_array_of_chars_instances - @chars.split(//).each do |character| - assert_kind_of ActiveSupport::Multibyte.proxy_class, character - end + def test_split_should_return_an_array_of_chars_instances + @chars.split(//).each do |character| + assert_kind_of ActiveSupport::Multibyte.proxy_class, character end + end - def test_indexed_insert_accepts_fixnums - @chars[2] = 32 - assert_equal 'こに わ', @chars - end + def test_indexed_insert_accepts_fixnums + @chars[2] = 32 + assert_equal 'こに わ', @chars + end - def test_overridden_bang_methods_return_self - [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!, :capitalize!].each do |method| - assert_equal @chars.object_id, @chars.send(method).object_id - end + def test_overridden_bang_methods_return_self + [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!, :capitalize!].each do |method| + assert_equal @chars.object_id, @chars.send(method).object_id end + end - def test_overridden_bang_methods_change_wrapped_string - [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!].each do |method| - original = ' Café ' - proxy = chars(original.dup) - proxy.send(method) - assert_not_equal original, proxy.to_s - end - proxy = chars('òu') - proxy.capitalize! - assert_equal 'Òu', proxy.to_s + def test_overridden_bang_methods_change_wrapped_string + [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!].each do |method| + original = ' Café ' + proxy = chars(original.dup) + proxy.send(method) + assert_not_equal original, proxy.to_s end + proxy = chars('òu') + proxy.capitalize! + assert_equal 'Òu', proxy.to_s end if RUBY_VERSION >= '1.9' @@ -151,11 +148,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase def test_identity assert_equal @chars, @chars assert @chars.eql?(@chars) - if RUBY_VERSION <= '1.9' - assert !@chars.eql?(UNICODE_STRING) - else - assert @chars.eql?(UNICODE_STRING) - end + assert !@chars.eql?(UNICODE_STRING) end def test_string_methods_are_chainable @@ -207,7 +200,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase assert_equal 'こわにちわ', @chars.insert(1, 'わ') assert_equal 'こわわわにちわ', @chars.insert(2, 'わわ') assert_equal 'わこわわわにちわ', @chars.insert(0, 'わ') - assert_equal 'わこわわわにちわ', @chars.wrapped_string if RUBY_VERSION < '1.9' + assert_equal 'わこわわわにちわ', @chars.wrapped_string end def test_insert_should_be_destructive @@ -330,7 +323,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase assert_raise(ArgumentError) { @chars.center } end - def test_center_should_count_charactes_instead_of_bytes + def test_center_should_count_characters_instead_of_bytes assert_equal UNICODE_STRING, @chars.center(-3) assert_equal UNICODE_STRING, @chars.center(0) assert_equal UNICODE_STRING, @chars.center(4) diff --git a/activesupport/test/multibyte_conformance.rb b/activesupport/test/multibyte_conformance.rb index caae4791b4..6ec9a839af 100644 --- a/activesupport/test/multibyte_conformance.rb +++ b/activesupport/test/multibyte_conformance.rb @@ -28,7 +28,7 @@ end class MultibyteConformanceTest < Test::Unit::TestCase include MultibyteTestHelpers - UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::UNICODE_VERSION}/ucd" + UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" UNIDATA_FILE = '/NormalizationTest.txt' CACHE_DIR = File.join(Dir.tmpdir, 'cache') diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb index 405c7c2108..26a41579c2 100644 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ b/activesupport/test/multibyte_unicode_database_test.rb @@ -1,21 +1,25 @@ # encoding: utf-8 require 'abstract_unit' + class MultibyteUnicodeDatabaseTest < Test::Unit::TestCase + + include ActiveSupport::Multibyte::Unicode + def setup - @ucd = ActiveSupport::Multibyte::UnicodeDatabase.new + @ucd = UnicodeDatabase.new end - ActiveSupport::Multibyte::UnicodeDatabase::ATTRIBUTES.each do |attribute| + UnicodeDatabase::ATTRIBUTES.each do |attribute| define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do @ucd.expects(:load) @ucd.send(attribute) end end - + def test_load @ucd.load - ActiveSupport::Multibyte::UnicodeDatabase::ATTRIBUTES.each do |attribute| + UnicodeDatabase::ATTRIBUTES.each do |attribute| assert @ucd.send(attribute).length > 1 end end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index a326e29d27..1d76c77ade 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [beta 4/release candidate] (unreleased)* + +* Version bump + + *Rails 3.0.0 [beta 3] (April 13th, 2010)* * Renamed config.cookie_secret to config.secret_token and pass it as env key. [JV] diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index ee44acc2fa..cd4a3dce4e 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -17,7 +17,7 @@ module Rails private %w(template copy_file directory empty_directory inside empty_directory_with_gitkeep create_file chmod shebang).each do |method| - class_eval <<-RUBY + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) @generator.send(:#{method}, *args, &block) end diff --git a/railties/test/application/rackup_test.rb b/railties/test/application/rackup_test.rb index 3790de721c..863950c04f 100644 --- a/railties/test/application/rackup_test.rb +++ b/railties/test/application/rackup_test.rb @@ -35,7 +35,7 @@ module ApplicationTests # keep it working for now test "deprecated ActionController::Dispatcher still works" do rackup - assert_kind_of? Rails::Application, ActionController::Dispatcher.new + assert_kind_of Rails::Application, ActionController::Dispatcher.new end test "the config object is available on the application object" do diff --git a/railties/test/railties/i18n_railtie_test.rb b/railties/test/railties/i18n_railtie_test.rb index 51684aa838..2b1950b3d5 100644 --- a/railties/test/railties/i18n_railtie_test.rb +++ b/railties/test/railties/i18n_railtie_test.rb @@ -9,12 +9,6 @@ module RailtiesTest boot_rails FileUtils.rm_rf("#{app_path}/config/environments") require "rails/all" - @old_path = I18n.load_path - end - - def teardown - I18n.load_path = @old_path || [] - I18n.backend = nil end def load_app @@ -42,7 +36,7 @@ module RailtiesTest end test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do - I18n.backend = Class.new { include I18n::Backend::Base }.new # can't uninclude modules, so use a tmp backend class + I18n.backend = Class.new { include I18n::Backend::Base }.new load_app assert_no_fallbacks end @@ -50,6 +44,15 @@ module RailtiesTest test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings" do I18n::Railtie.config.i18n.fallbacks = true load_app + assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_fallbacks :de => [:de, :en] + end + + test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings even when backend changes" do + I18n::Railtie.config.i18n.fallbacks = true + I18n::Railtie.config.i18n.backend = Class.new { include I18n::Backend::Base }.new + load_app + assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) assert_fallbacks :de => [:de, :en] end |