From a17027d13a48e1e64b14a28e7d58e341812f8cb4 Mon Sep 17 00:00:00 2001 From: Pratik Naik <pratiknaik@gmail.com> Date: Sat, 13 Sep 2008 20:28:01 +0100 Subject: Merge docrails --- activesupport/lib/active_support/core_ext/array/grouping.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index dd1484f8fa..f782f8facf 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -7,16 +7,16 @@ module ActiveSupport #:nodoc: # Splits or iterates over the array in groups of size +number+, # padding any remaining slots with +fill_with+ unless it is +false+. # - # %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group} # ["1", "2", "3"] # ["4", "5", "6"] # ["7", nil, nil] # - # %w(1 2 3).in_groups_of(2, ' ') {|g| p g} + # %w(1 2 3).in_groups_of(2, ' ') {|group| p group} # ["1", "2"] # ["3", " "] # - # %w(1 2 3).in_groups_of(2, false) {|g| p g} + # %w(1 2 3).in_groups_of(2, false) {|group| p group} # ["1", "2"] # ["3"] def in_groups_of(number, fill_with = nil) @@ -42,17 +42,17 @@ module ActiveSupport #:nodoc: # Splits or iterates over the array in +number+ of groups, padding any # remaining slots with +fill_with+ unless it is +false+. # - # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|g| p g} + # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group} # ["1", "2", "3", "4"] # ["5", "6", "7", nil] # ["8", "9", "10", nil] # - # %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group} # ["1", "2", "3"] # ["4", "5", " "] # ["6", "7", " "] # - # %w(1 2 3 4 5 6 7).in_groups(3, false) {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group} # ["1", "2", "3"] # ["4", "5"] # ["6", "7"] -- cgit v1.2.3 From d28c724b4074605445d2834888ad280778f0af6a Mon Sep 17 00:00:00 2001 From: Michael Koziarski <michael@koziarski.com> Date: Sun, 14 Sep 2008 09:44:29 +0200 Subject: Make sure the permissions check file is closed before being unlinked. [#1035 state:committed] --- activesupport/lib/active_support/core_ext/file/atomic.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 4d3cf5423f..f988eff3d9 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -28,7 +28,7 @@ module ActiveSupport #:nodoc: rescue Errno::ENOENT # No old permissions, write a temp file to determine the defaults check_name = ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" - new(check_name, "w") + open(check_name, "w") { } old_stat = stat(check_name) unlink(check_name) end -- cgit v1.2.3 From bfa12d7a02ce0e84fcd2b83f2ce6fee1386757e3 Mon Sep 17 00:00:00 2001 From: Clemens Kofler <clemens@railway.at> Date: Thu, 11 Sep 2008 12:51:16 +0200 Subject: Introduce convenience methods past?, today? and future? for Date and Time classes to facilitate Date/Time comparisons. --- .../active_support/core_ext/date/calculations.rb | 33 +++++--- .../core_ext/date_time/calculations.rb | 24 ++++-- .../active_support/core_ext/time/calculations.rb | 35 +++++--- activesupport/lib/active_support/time_with_zone.rb | 92 ++++++++++++---------- 4 files changed, 120 insertions(+), 64 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index b5180c9592..43d70c7013 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -20,18 +20,33 @@ module ActiveSupport #:nodoc: def yesterday ::Date.today.yesterday end - + # Returns a new Date representing the date 1 day after today (i.e. tomorrow's date). def tomorrow ::Date.today.tomorrow end - + # Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today. def current ::Time.zone_default ? ::Time.zone.today : ::Date.today end end - + + # Tells whether the Date object's date lies in the past + def past? + self < ::Date.current + end + + # Tells whether the Date object's date is today + def today? + self.to_date == ::Date.current # we need the to_date because of DateTime + end + + # Tells whether the Date object's date lies in the future + def future? + self > ::Date.current + end + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) # and then subtracts the specified number of seconds def ago(seconds) @@ -57,7 +72,7 @@ module ActiveSupport #:nodoc: def end_of_day to_time.end_of_day end - + def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other other.since(self) @@ -65,7 +80,7 @@ module ActiveSupport #:nodoc: plus_without_duration(other) end end - + def minus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other plus_with_duration(-other) @@ -73,8 +88,8 @@ module ActiveSupport #:nodoc: minus_without_duration(other) end end - - # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with + + # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>. def advance(options) d = self @@ -98,7 +113,7 @@ module ActiveSupport #:nodoc: options[:day] || self.day ) end - + # Returns a new Date/DateTime representing the time a number of specified months ago def months_ago(months) advance(:months => -months) @@ -161,7 +176,7 @@ module ActiveSupport #:nodoc: days_into_week = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6} result = (self + 7).beginning_of_week + days_into_week[day] self.acts_like?(:time) ? result.change(:hour => 0) : result - end + end # Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00) def beginning_of_month diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 155c961a91..0099431e9d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -7,7 +7,7 @@ module ActiveSupport #:nodoc: module Calculations def self.included(base) #:nodoc: base.extend ClassMethods - + base.class_eval do alias_method :compare_without_coercion, :<=> alias_method :<=>, :compare_with_coercion @@ -19,6 +19,20 @@ module ActiveSupport #:nodoc: def local_offset ::Time.local(2007).utc_offset.to_r / 86400 end + + def current + ::Time.zone_default ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime + end + end + + # Tells whether the DateTime object's datetime lies in the past + def past? + self < ::DateTime.current + end + + # Tells whether the DateTime object's datetime lies in the future + def future? + self > ::DateTime.current end # Seconds since midnight: DateTime.now.seconds_since_midnight @@ -78,7 +92,7 @@ module ActiveSupport #:nodoc: def end_of_day change(:hour => 23, :min => 59, :sec => 59) end - + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0 # # Example: @@ -89,17 +103,17 @@ module ActiveSupport #:nodoc: new_offset(0) end alias_method :getutc, :utc - + # Returns true if offset == 0 def utc? offset == 0 end - + # Returns the offset value in seconds def utc_offset (offset * 86400).to_i end - + # Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime def compare_with_coercion(other) other = other.comparable_time if other.respond_to?(:comparable_time) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index cd234c9b89..070f72c854 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -9,13 +9,13 @@ module ActiveSupport #:nodoc: base.class_eval do alias_method :plus_without_duration, :+ alias_method :+, :plus_with_duration - + alias_method :minus_without_duration, :- alias_method :-, :minus_with_duration - + alias_method :minus_without_coercion, :- alias_method :-, :minus_with_coercion - + alias_method :compare_without_coercion, :<=> alias_method :<=>, :compare_with_coercion end @@ -28,9 +28,9 @@ module ActiveSupport #:nodoc: def ===(other) other.is_a?(::Time) end - - # Return the number of days in the given month. - # If no year is specified, it will use the current year. + + # Return the number of days in the given month. + # If no year is specified, it will use the current year. def days_in_month(month, year = now.year) return 29 if month == 2 && ::Date.gregorian_leap?(year) COMMON_YEAR_DAYS_IN_MONTH[month] @@ -57,6 +57,21 @@ module ActiveSupport #:nodoc: end end + # Tells whether the Time object's time lies in the past + def past? + self < ::Time.current + end + + # Tells whether the Time object's time is today + def today? + self.to_date == ::Date.current + end + + # Tells whether the Time object's time lies in the future + def future? + self > ::Time.current + end + # Seconds since midnight: Time.now.seconds_since_midnight def seconds_since_midnight self.to_i - self.change(:hour => 0).to_i + (self.usec/1.0e+6) @@ -106,7 +121,7 @@ module ActiveSupport #:nodoc: (seconds.abs >= 86400 && initial_dst != final_dst) ? f + (initial_dst - final_dst).hours : f end rescue - self.to_datetime.since(seconds) + self.to_datetime.since(seconds) end alias :in :since @@ -199,7 +214,7 @@ module ActiveSupport #:nodoc: change(:day => last_day, :hour => 23, :min => 59, :sec => 59, :usec => 0) end alias :at_end_of_month :end_of_month - + # Returns a new Time representing the start of the quarter (1st of january, april, july, october, 0:00) def beginning_of_quarter beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month }) @@ -249,7 +264,7 @@ module ActiveSupport #:nodoc: minus_without_duration(other) end end - + # Time#- can also be used to determine the number of seconds between two Time instances. # We're layering on additional behavior so that ActiveSupport::TimeWithZone instances # are coerced into values that Time#- will recognize @@ -257,7 +272,7 @@ module ActiveSupport #:nodoc: other = other.comparable_time if other.respond_to?(:comparable_time) minus_without_coercion(other) end - + # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances # can be chronologically compared with a Time def compare_with_coercion(other) diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 75591b7c34..44088f436e 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,6 +1,6 @@ require 'tzinfo' module ActiveSupport - # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are + # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are # limited to UTC and the system's <tt>ENV['TZ']</tt> zone. # # You shouldn't ever need to create a TimeWithZone instance directly via <tt>new</tt> -- instead, Rails provides the methods @@ -32,12 +32,12 @@ module ActiveSupport class TimeWithZone include Comparable attr_reader :time_zone - + def initialize(utc_time, time_zone, local_time = nil, period = nil) @utc, @time_zone, @time = utc_time, time_zone, local_time @period = @utc ? period : get_period_and_ensure_valid_local_time end - + # Returns a Time or DateTime instance that represents the time in +time_zone+. def time @time ||= period.to_local(@utc) @@ -51,7 +51,7 @@ module ActiveSupport alias_method :getgm, :utc alias_method :getutc, :utc alias_method :gmtime, :utc - + # Returns the underlying TZInfo::TimezonePeriod. def period @period ||= time_zone.period_for_utc(@utc) @@ -62,38 +62,38 @@ module ActiveSupport return self if time_zone == new_zone utc.in_time_zone(new_zone) end - + # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your system's <tt>ENV['TZ']</tt> zone def localtime utc.getlocal end alias_method :getlocal, :localtime - + def dst? period.dst? end alias_method :isdst, :dst? - + def utc? time_zone.name == 'UTC' end alias_method :gmt?, :utc? - + def utc_offset period.utc_total_offset end alias_method :gmt_offset, :utc_offset alias_method :gmtoff, :utc_offset - + def formatted_offset(colon = true, alternate_utc_string = nil) utc? && alternate_utc_string || utc_offset.to_utc_offset_s(colon) end - + # Time uses +zone+ to display the time zone abbreviation, so we're duck-typing it. def zone period.zone_identifier.to_s end - + def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end @@ -122,7 +122,7 @@ module ActiveSupport %("#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}") end end - + def to_yaml(options = {}) if options.kind_of?(YAML::Emitter) utc.to_yaml(options) @@ -130,19 +130,19 @@ module ActiveSupport time.to_yaml(options).gsub('Z', formatted_offset(true, 'Z')) end end - + def httpdate utc.httpdate end - + def rfc2822 to_s(:rfc822) end alias_method :rfc822, :rfc2822 - + # <tt>:db</tt> format outputs time in UTC; all others output time in local. # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly. - def to_s(format = :default) + def to_s(format = :default) return utc.to_s(format) if format == :db if formatter = ::Time::DATE_FORMATS[format] formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter) @@ -150,27 +150,39 @@ module ActiveSupport "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format end end - + # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and +formatted_offset+, respectively, before passing to # Time#strftime, so that zone information is correct def strftime(format) format = format.gsub('%Z', zone).gsub('%z', formatted_offset(false)) time.strftime(format) end - + # Use the time in UTC for comparisons. def <=>(other) utc <=> other end - + def between?(min, max) utc.between?(min, max) end - + + def past? + utc.past? + end + + def today? + utc.today? + end + + def future? + utc.future? + end + def eql?(other) utc == other end - + def +(other) # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, # otherwise move forward from #utc, for accuracy when moving across DST boundaries @@ -194,7 +206,7 @@ module ActiveSupport result.in_time_zone(time_zone) end end - + def since(other) # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, # otherwise move forward from #utc, for accuracy when moving across DST boundaries @@ -204,7 +216,7 @@ module ActiveSupport utc.since(other).in_time_zone(time_zone) end end - + def ago(other) since(-other) end @@ -218,7 +230,7 @@ module ActiveSupport utc.advance(options).in_time_zone(time_zone) end end - + %w(year mon month day mday hour min sec).each do |method_name| class_eval <<-EOV def #{method_name} @@ -226,45 +238,45 @@ module ActiveSupport end EOV end - + def usec time.respond_to?(:usec) ? time.usec : 0 end - + def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end - + def to_f utc.to_f - end - + end + def to_i utc.to_i end alias_method :hash, :to_i alias_method :tv_sec, :to_i - + # A TimeWithZone acts like a Time, so just return +self+. def to_time self end - + def to_datetime utc.to_datetime.new_offset(Rational(utc_offset, 86_400)) end - + # So that +self+ <tt>acts_like?(:time)</tt>. def acts_like_time? true end - + # Say we're a Time to thwart type checking. def is_a?(klass) klass == ::Time || super end alias_method :kind_of?, :is_a? - + # Neuter freeze because freezing can cause problems with lazy loading of attributes. def freeze self @@ -273,7 +285,7 @@ module ActiveSupport def marshal_dump [utc, time_zone.name, time] end - + def marshal_load(variables) initialize(variables[0].utc, ::Time.__send__(:get_zone, variables[1]), variables[2].utc) end @@ -290,10 +302,10 @@ module ActiveSupport result = time.__send__(sym, *args, &block) result.acts_like?(:time) ? self.class.new(nil, time_zone, result) : result end - - private + + private def get_period_and_ensure_valid_local_time - # we don't want a Time.local instance enforcing its own DST rules as well, + # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin @@ -304,11 +316,11 @@ module ActiveSupport retry end end - + def transfer_time_values_to_utc_constructor(time) ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0) end - + def duration_of_variable_length?(obj) ActiveSupport::Duration === obj && obj.parts.flatten.detect {|p| [:years, :months, :days].include? p } end -- cgit v1.2.3 From cce7ae54663243a224e9871f1aac2388842b0c3a Mon Sep 17 00:00:00 2001 From: gbuesing <gbuesing@gmail.com> Date: Sun, 14 Sep 2008 22:56:32 -0500 Subject: Add thorough tests for Time-object #past?, #future? and #today. Fix TimeWithZone #today? to use #time instead of #utc for date comparison. Update changelog. [#720 state:resolved] --- activesupport/lib/active_support/time_with_zone.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 44088f436e..54cf945251 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -172,7 +172,7 @@ module ActiveSupport end def today? - utc.today? + time.today? end def future? -- cgit v1.2.3 From 157141b2949b845e372ee703bfd6fba3ffb00415 Mon Sep 17 00:00:00 2001 From: gbuesing <gbuesing@gmail.com> Date: Sun, 14 Sep 2008 23:07:48 -0500 Subject: TimeWithZone #wday, #yday and #to_date avoid trip through #method_missing --- activesupport/lib/active_support/time_with_zone.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 54cf945251..b7b8807c6d 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -231,7 +231,7 @@ module ActiveSupport end end - %w(year mon month day mday hour min sec).each do |method_name| + %w(year mon month day mday wday yday hour min sec to_date).each do |method_name| class_eval <<-EOV def #{method_name} time.#{method_name} -- cgit v1.2.3 From 4dae3649f062137347bac43cd0708207d2a94d66 Mon Sep 17 00:00:00 2001 From: Michael Koziarski <michael@koziarski.com> Date: Tue, 16 Sep 2008 16:50:14 +0200 Subject: Enhance the test "some string" method to support creating 'pending' tests. If no block is provided to the test method, a default test will be generated which simply flunks. This makes it easy for you to generate a list of what you intend to do, then flesh it out with actual tests. --- activesupport/lib/active_support/test_case.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 0f531b0c79..197e73b3e8 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -12,7 +12,13 @@ module ActiveSupport test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym defined = instance_method(test_name) rescue false raise "#{test_name} is already defined in #{self}" if defined - define_method(test_name, &block) + if block_given? + define_method(test_name, &block) + else + define_method(test_name) do + flunk "No implementation provided for #{name}" + end + end end end end -- cgit v1.2.3 From e7cb8c844ad9a5b3260c7e369b288d0792576765 Mon Sep 17 00:00:00 2001 From: Duff OMelia <duff@omelia.org> Date: Thu, 18 Sep 2008 08:51:19 -0500 Subject: Ensure old buffers get properly cleared to avoid leaking memory Signed-off-by: Joshua Peek <josh@joshpeek.com> --- activesupport/lib/active_support/buffered_logger.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index 6553d72b4f..77e0b1d33f 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -116,7 +116,7 @@ module ActiveSupport end def clear_buffer - @buffer[Thread.current] = [] + @buffer.delete(Thread.current) end end end -- cgit v1.2.3 From 79f55de9c5e3ff1f8d9e767c5af21ba31be4cfba Mon Sep 17 00:00:00 2001 From: Carlos Brando <eduardobrando@gmail.com> Date: Fri, 19 Sep 2008 09:06:35 -0500 Subject: Fixed Time#end_of_quarter to not blow up on May 31st [#313 state:resolved] Signed-off-by: Joshua Peek <josh@joshpeek.com> --- activesupport/lib/active_support/core_ext/time/calculations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 070f72c854..3cc6d59907 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -223,7 +223,7 @@ module ActiveSupport #:nodoc: # Returns a new Time representing the end of the quarter (last day of march, june, september, december, 23:59:59) def end_of_quarter - change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month + beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month end alias :at_end_of_quarter :end_of_quarter -- cgit v1.2.3 From 8cb7d460439a9b20a80a77b6370c1107233d1cbd Mon Sep 17 00:00:00 2001 From: Sven Fuchs <svenfuchs@artweb-design.de> Date: Sun, 14 Sep 2008 13:36:06 +0200 Subject: I18n: move old-style interpolation syntax deprecation to Active Record. [#1044 state:resolved] Signed-off-by: Pratik Naik <pratiknaik@gmail.com> --- .../lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb | 7 ------- 1 file changed, 7 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb index e2d19cdd45..ff15d83f4e 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb @@ -4,7 +4,6 @@ module I18n module Backend class Simple INTERPOLATION_RESERVED_KEYS = %w(scope default) - DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' } MATCH = /(\\\\)?\{\{([^\}]+)\}\}/ # Accepts a list of paths to translation files. Loads translations from @@ -120,12 +119,6 @@ module I18n def interpolate(locale, string, values = {}) return string unless string.is_a?(String) - string = string.gsub(/%d|%s/) do |s| - instead = DEPRECATED_INTERPOLATORS[s] - ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead." - instead - end - if string.respond_to?(:force_encoding) original_encoding = string.encoding string.force_encoding(Encoding::BINARY) -- cgit v1.2.3 From a3b7fa78bfdc33e45e39c095b67e02d50a2c7bea Mon Sep 17 00:00:00 2001 From: Sven Fuchs <svenfuchs@artweb-design.de> Date: Mon, 15 Sep 2008 10:26:50 +0200 Subject: I18n: Introduce I18n.load_path in favor of I18n.load_translations and change Simple backend to load translations lazily. [#1048 state:resolved] Signed-off-by: Pratik Naik <pratiknaik@gmail.com> --- activesupport/lib/active_support.rb | 2 +- .../lib/active_support/vendor/i18n-0.0.1/i18n.rb | 27 ++++++++++++++-------- .../vendor/i18n-0.0.1/i18n/backend/simple.rb | 14 +++++++++-- 3 files changed, 30 insertions(+), 13 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 7e34a871e3..b30faff06d 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -56,7 +56,7 @@ require 'active_support/time_with_zone' require 'active_support/secure_random' -I18n.load_translations File.dirname(__FILE__) + '/active_support/locale/en-US.yml' +I18n.load_path << File.dirname(__FILE__) + '/active_support/locale/en-US.yml' Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector') Dependencies = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Dependencies', 'ActiveSupport::Dependencies') diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb index 0988ea8f44..344c77aecf 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb @@ -10,7 +10,8 @@ require 'i18n/exceptions' module I18n @@backend = nil - @@default_locale = 'en-US' + @@load_path = nil + @@default_locale = :'en-US' @@exception_handler = :default_exception_handler class << self @@ -49,14 +50,22 @@ module I18n @@exception_handler = exception_handler end - # Allows client libraries to pass arguments that specify a source for - # translation data to be loaded by the backend. The backend defines - # acceptable sources. + # Allow clients to register paths providing translation data sources. The + # backend defines acceptable sources. + # # E.g. the provided SimpleBackend accepts a list of paths to translation # files which are either named *.rb and contain plain Ruby Hashes or are - # named *.yml and contain YAML data.) - def load_translations(*args) - backend.load_translations(*args) + # named *.yml and contain YAML data. So for the SimpleBackend clients may + # register translation files like this: + # I18n.load_path << 'path/to/locale/en-US.yml' + def load_path + @@load_path ||= [] + end + + # Sets the load path instance. Custom implementations are expected to + # behave like a Ruby Array. + def load_path=(load_path) + @@load_path = load_path end # Translates, pluralizes and interpolates a given key using a given locale, @@ -175,6 +184,4 @@ module I18n keys.flatten.map{|k| k.to_sym} end end -end - - +end \ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb index ff15d83f4e..2dbaf8a405 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb @@ -1,4 +1,4 @@ -require 'strscan' +require 'yaml' module I18n module Backend @@ -59,7 +59,16 @@ module I18n object.strftime(format) end + def initialized? + @initialized ||= false + end + protected + + def init_translations + load_translations(*I18n.load_path) + @initialized = true + end def translations @translations ||= {} @@ -72,6 +81,7 @@ module I18n # <tt>%w(currency format)</tt>. def lookup(locale, key, scope = []) return unless key + init_translations unless initialized? keys = I18n.send :normalize_translation_keys, locale, key, scope keys.inject(translations){|result, k| result[k.to_sym] or return nil } end @@ -94,7 +104,7 @@ module I18n rescue MissingTranslationData nil end - + # Picks a translation from an array according to English pluralization # rules. It will pick the first translation if count is not equal to 1 # and the second translation if it is equal to 1. Other backends can -- cgit v1.2.3 From 22f75d539dca7b6f33cbf86e4e9d1944bb22731f Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:21:30 +0200 Subject: Simplify ActiveSupport::Multibyte and make it run on Ruby 1.9. * Unicode methods are now defined directly on Chars instead of a handler * Updated Unicode database to Unicode 5.1.0 * Improved documentation --- .../lib/active_support/core_ext/string.rb | 6 +- .../active_support/core_ext/string/multibyte.rb | 81 +++ .../lib/active_support/core_ext/string/unicode.rb | 66 -- activesupport/lib/active_support/multibyte.rb | 36 +- .../lib/active_support/multibyte/chars.rb | 782 +++++++++++++++++---- .../lib/active_support/multibyte/exceptions.rb | 7 + .../multibyte/generators/generate_tables.rb | 149 ---- .../multibyte/handlers/passthru_handler.rb | 9 - .../multibyte/handlers/utf8_handler.rb | 564 --------------- .../multibyte/handlers/utf8_handler_proc.rb | 43 -- .../active_support/multibyte/unicode_database.rb | 71 ++ .../lib/active_support/values/unicode_tables.dat | Bin 656156 -> 710734 bytes 12 files changed, 848 insertions(+), 966 deletions(-) create mode 100644 activesupport/lib/active_support/core_ext/string/multibyte.rb delete mode 100644 activesupport/lib/active_support/core_ext/string/unicode.rb create mode 100644 activesupport/lib/active_support/multibyte/exceptions.rb delete mode 100755 activesupport/lib/active_support/multibyte/generators/generate_tables.rb delete mode 100644 activesupport/lib/active_support/multibyte/handlers/passthru_handler.rb delete mode 100644 activesupport/lib/active_support/multibyte/handlers/utf8_handler.rb delete mode 100644 activesupport/lib/active_support/multibyte/handlers/utf8_handler_proc.rb create mode 100644 activesupport/lib/active_support/multibyte/unicode_database.rb (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index 7ff2f11eff..16c544a577 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -1,9 +1,11 @@ +# encoding: utf-8 + require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/string/access' require 'active_support/core_ext/string/starts_ends_with' require 'active_support/core_ext/string/iterators' -require 'active_support/core_ext/string/unicode' +require 'active_support/core_ext/string/multibyte' require 'active_support/core_ext/string/xchar' require 'active_support/core_ext/string/filters' require 'active_support/core_ext/string/behavior' @@ -15,6 +17,6 @@ class String #:nodoc: include ActiveSupport::CoreExtensions::String::Inflections include ActiveSupport::CoreExtensions::String::StartsEndsWith include ActiveSupport::CoreExtensions::String::Iterators - include ActiveSupport::CoreExtensions::String::Unicode include ActiveSupport::CoreExtensions::String::Behavior + include ActiveSupport::CoreExtensions::String::Multibyte end diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb new file mode 100644 index 0000000000..5a2dc36f72 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -0,0 +1,81 @@ +# encoding: utf-8 + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Implements multibyte methods for easier access to multibyte characters in a String instance. + module Multibyte + unless '1.9'.respond_to?(:force_encoding) + # +mb_chars+ is a multibyte safe proxy method for string methods. + # + # In Ruby 1.8 and older it creates and returns an instance of the ActiveSupport::Multibyte::Chars class which + # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy + # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsuled string. + # + # name = 'Claus Müller' + # name.reverse #=> "rell??M sualC" + # name.length #=> 13 + # + # name.mb_chars.reverse.to_s #=> "rellüM sualC" + # name.mb_chars.length #=> 12 + # + # In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware so we don't need + # a proxy class any more. This means that +mb_chars+ makes it easier to write code that runs on multiple Ruby + # versions. + # + # == Method chaining + # + # All the methods on the Chars proxy which normally return a string will return a Chars object. This allows + # method chaining on the result of any of these methods. + # + # name.mb_chars.reverse.length #=> 12 + # + # == Interoperability and configuration + # + # The Char object tries to be as interchangeable with String objects as possible: sorting and comparing between + # String and Char work like expected. The bang! methods change the internal string representation in the Chars + # object. Interoperability problems can be resolved easily with a +to_s+ call. + # + # 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) + 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 + + unless '1.8.7 and later'.respond_to?(:chars) + alias chars mb_chars + end + else + # In Ruby 1.9 and newer +mb_chars+ returns self. In Ruby 1.8 and older +mb_chars+ creates and returns an + # Unicode safe proxy for string operations, this makes it easier to write code that runs on multiple Ruby + # versions. + def mb_chars + self + end + + # Returns true if the string has valid UTF-8 encoding. + def is_utf8? + case encoding + when Encoding::UTF_8 + valid_encoding? + when Encoding::ASCII_8BIT, Encoding::US_ASCII + dup.force_encoding(Encoding::UTF_8).valid_encoding? + else + false + end + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/string/unicode.rb b/activesupport/lib/active_support/core_ext/string/unicode.rb deleted file mode 100644 index 666f7bcb65..0000000000 --- a/activesupport/lib/active_support/core_ext/string/unicode.rb +++ /dev/null @@ -1,66 +0,0 @@ -module ActiveSupport #:nodoc: - module CoreExtensions #:nodoc: - module String #:nodoc: - # Define methods for handling unicode data. - module Unicode - def self.append_features(base) - if '1.8.7 and later'.respond_to?(:chars) - base.class_eval { remove_method :chars } - end - super - end - - unless '1.9'.respond_to?(:force_encoding) - # +chars+ is a Unicode safe proxy for string methods. It creates and returns an instance of the - # ActiveSupport::Multibyte::Chars class which encapsulates the original string. A Unicode safe version of all - # the String methods are defined on this proxy class. Undefined methods are forwarded to String, so all of the - # string overrides can also be called through the +chars+ proxy. - # - # name = 'Claus Müller' - # name.reverse # => "rell??M sualC" - # name.length # => 13 - # - # name.chars.reverse.to_s # => "rellüM sualC" - # name.chars.length # => 12 - # - # - # All the methods on the chars proxy which normally return a string will return a Chars object. This allows - # method chaining on the result of any of these methods. - # - # name.chars.reverse.length # => 12 - # - # The Char object tries to be as interchangeable with String objects as possible: sorting and comparing between - # String and Char work like expected. The bang! methods change the internal string representation in the Chars - # object. Interoperability problems can be resolved easily with a +to_s+ call. - # - # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars and - # ActiveSupport::Multibyte::Handlers::UTF8Handler. - def chars - ActiveSupport::Multibyte::Chars.new(self) - 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::Handlers::UTF8Handler.consumes?(self) - end - else - def chars #:nodoc: - self - end - - def is_utf8? #:nodoc: - case encoding - when Encoding::UTF_8 - valid_encoding? - when Encoding::ASCII_8BIT - dup.force_encoding('UTF-8').valid_encoding? - else - false - end - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb index 27c0d180a5..63c0d50166 100644 --- a/activesupport/lib/active_support/multibyte.rb +++ b/activesupport/lib/active_support/multibyte.rb @@ -1,9 +1,33 @@ -module ActiveSupport +# encoding: utf-8 + +require 'active_support/multibyte/chars' +require 'active_support/multibyte/exceptions' +require 'active_support/multibyte/unicode_database' + +module ActiveSupport #:nodoc: module Multibyte #:nodoc: - DEFAULT_NORMALIZATION_FORM = :kc + # A list of all available normalization forms. See http://www.unicode.org/reports/tr15/tr15-29.html for more + # information about normalization. NORMALIZATIONS_FORMS = [:c, :kc, :d, :kd] - UNICODE_VERSION = '5.0.0' - end -end -require 'active_support/multibyte/chars' + # 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 NORMALIZATIONS_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. + # + # Example: + # ActiveSupport::Multibyte.proxy_class = CharsForUTF32 + mattr_accessor :proxy_class + self.proxy_class = ActiveSupport::Multibyte::Chars + end +end \ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index de2c83f8d1..c05419bfbf 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -1,142 +1,670 @@ -require 'active_support/multibyte/handlers/utf8_handler' -require 'active_support/multibyte/handlers/passthru_handler' - -# Encapsulates all the functionality related to the Chars proxy. -module ActiveSupport::Multibyte #:nodoc: - # Chars enables you to work transparently with multibyte encodings in the Ruby String class without having extensive - # knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an - # encoding safe manner. All the normal String methods are also implemented on the proxy. - # - # String methods are proxied through the Chars object, and can be accessed through the +chars+ method. Methods - # which would normally return a String object now return a Chars object so methods can be chained. - # - # "The Perfect String ".chars.downcase.strip.normalize # => "the perfect string" - # - # Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made. - # If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them. - # - # bad.explicit_checking_method "T".chars.downcase.to_s - # - # The actual operations on the string are delegated to handlers. Theoretically handlers can be implemented for - # any encoding, but the default handler handles UTF-8. This handler is set during initialization, if you want to - # use you own handler, you can set it on the Chars class. Look at the UTF8Handler source for an example how to - # implement your own handler. If you your own handler to work on anything but UTF-8 you probably also - # want to override Chars#handler. - # - # ActiveSupport::Multibyte::Chars.handler = MyHandler - # - # Note that a few methods are defined on Chars instead of the handler because they are defined on Object or Kernel - # and method_missing can't catch them. - class Chars - - attr_reader :string # The contained string - alias_method :to_s, :string - - include Comparable - - # The magic method to make String and Chars comparable - def to_str - # Using any other ways of overriding the String itself will lead you all the way from infinite loops to - # core dumps. Don't go there. - @string - end +# encoding: utf-8 + +module ActiveSupport #:nodoc: + module Multibyte #:nodoc: + # Chars enables you to work transparently with multibyte encodings in the Ruby String class without having extensive + # knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an + # encoding safe manner. All the normal String methods are also implemented on the proxy. + # + # String methods are proxied through the Chars object, and can be accessed through the +mb_chars+ method. Methods + # which would normally return a String object now return a Chars object so methods can be chained. + # + # "The Perfect String ".chars.downcase.strip.normalize #=> "the perfect string" + # + # Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made. + # If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them. + # + # bad.explicit_checking_method "T".chars.downcase.to_s + # + # The default Chars implementation assumes that the encoding of the string is UTF-8, if you want to handle different + # encodings you can write your own multibyte string handler and configure it through + # ActiveSupport::Multibyte.proxy_class. + # + # class CharsForUTF32 + # def size + # @wrapped_string.size / 4 + # end + # + # def self.accepts?(string) + # string.length % 4 == 0 + # end + # end + # + # 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/ + UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/ + + # Borrowed from the Kconv library by Shinji KONO - (also as seen on the W3C site) + UTF8_PAT = /\A(?: + [\x00-\x7f] | + [\xc2-\xdf] [\x80-\xbf] | + \xe0 [\xa0-\xbf] [\x80-\xbf] | + [\xe1-\xef] [\x80-\xbf] [\x80-\xbf] | + \xf0 [\x90-\xbf] [\x80-\xbf] [\x80-\xbf] | + [\xf1-\xf3] [\x80-\xbf] [\x80-\xbf] [\x80-\xbf] | + \xf4 [\x80-\x8f] [\x80-\xbf] [\x80-\xbf] + )*\z/xn + + attr_reader :wrapped_string + alias to_s wrapped_string + alias to_str wrapped_string + + # Creates a new Chars instance. +string+ is the wrapped string. + if '1.9'.respond_to?(:force_encoding) + def initialize(string) + @wrapped_string = string + @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen? + end + else + def initialize(string) + @wrapped_string = string + end + end + + # Forward all undefined methods to the wrapped string. + def method_missing(method, *args, &block) + if method.to_s =~ /!$/ + @wrapped_string.__send__(method, *args, &block) + self + else + chars(@wrapped_string.__send__(method, *args, &block)) + end + end + + # Returns +true+ if _obj_ responds to the given method. Private methods are included in the search + # only if the optional second parameter evaluates to +true+. + def respond_to?(method, include_private=false) + super || @wrapped_string.respond_to?(method, include_private) || false + end + + # Enable more predictable duck-typing on String-like classes. See Object#acts_like?. + def acts_like_string? + 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) + RUBY_VERSION < '1.9' && $KCODE == 'UTF8' && consumes?(string) + end - # Make duck-typing with String possible - def respond_to?(method, include_priv = false) - super || @string.respond_to?(method, include_priv) || - handler.respond_to?(method, include_priv) || - (method.to_s =~ /(.*)!/ && handler.respond_to?($1, include_priv)) || + # 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. + string.unpack('U*') + true + rescue ArgumentError false - end + end - # Enable more predictable duck-typing on String-like classes. See Object#acts_like?. - def acts_like_string? - true - end + include Comparable - # Create a new Chars instance. - def initialize(str) - @string = str.respond_to?(:string) ? str.string : str - end - - # Returns -1, 0 or +1 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 String.<=> - # for more details. - def <=>(other); @string <=> other.to_s; end - - # Works just like String#split, with the exception that the items in the resulting list are Chars - # instances instead of String. This makes chaining methods easier. - def split(*args) - @string.split(*args).map { |i| i.chars } - end - - # Gsub works exactly the same as gsub on a normal string. - def gsub(*a, &b); @string.gsub(*a, &b).chars; end - - # Like String.=~ only it returns the character offset (in codepoints) instead of the byte offset. - def =~(other) - handler.translate_offset(@string, @string =~ other) - end - - # Try to forward all undefined methods to the handler, when a method is not defined on the handler, send it to - # the contained string. Method_missing is also responsible for making the bang! methods destructive. - def method_missing(m, *a, &b) - begin - # Simulate methods with a ! at the end because we can't touch the enclosed string from the handlers. - if m.to_s =~ /^(.*)\!$/ && handler.respond_to?($1) - result = handler.send($1, @string, *a, &b) - if result == @string - result = nil + # Returns -1, 0 or +1 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 String.<=> + # for more details. + # + # Example: + # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 + def <=>(other) + @wrapped_string <=> other.to_s + 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 + + # Like String.=~ 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 + + # Works just like String#split, 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 + + # 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" + 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 the position of the passed argument in the string, counting in codepoints + # + # Example: + # 'Café périferôl'.mb_chars.index('ô') #=> 12 + def index(*args) + index = @wrapped_string.index(*args) + index ? (self.class.u_unpack(@wrapped_string.slice(0...index)).size) : nil + end + + # Works just like the indexed replace method on string, except instead of byte offsets you specify + # character offsets. + # + # Example: + # + # s = "Müller" + # s.chars[2] = "e" # Replace character with offset 2 + # s + # #=> "Müeler" + # + # s = "Müller" + # s.chars[1, 2] = "ö" # Replace 2 characters at character offset 1 + # s + # #=> "Möler" + def []=(*args) + replace_by = args.pop + # Indexed replace with regular expressions already works + if args.first.is_a?(Regexp) + @wrapped_string[*args] = replace_by + else + result = self.class.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] + max = args[1].nil? ? min : (min + args[1] - 1) + range = Range.new(min, max) + replace_by = [replace_by].pack('U') if replace_by.is_a?(Fixnum) + elsif args.first.is_a?(Range) + raise RangeError, "#{args[0]} out of range" if args[0].min >= result.length + range = args[0] else - @string.replace result + needle = args[0].to_s + min = index(needle) + max = min + self.class.u_unpack(needle).length - 1 + range = Range.new(min, max) end - elsif handler.respond_to?(m) - result = handler.send(m, @string, *a, &b) - else - result = @string.send(m, *a, &b) + result[range] = self.class.u_unpack(replace_by) + @wrapped_string.replace(result.pack('U*')) end - rescue Handlers::EncodingError - @string.replace handler.tidy_bytes(@string) - retry + self end - - if result.kind_of?(String) - result.chars - else - result + + # Works just like String#rjust, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".chars.rjust(8).to_s + # #=> " ¾ cup" + # + # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace + # #=> " ¾ cup" + def rjust(integer, padstr=' ') + justify(integer, :right, padstr) end - end - - # Set the handler class for the Char objects. - def self.handler=(klass) - @@handler = klass - end - # Returns the proper handler for the contained string depending on $KCODE and the encoding of the string. This - # method is used internally to always redirect messages to the proper classes depending on the context. - def handler - if utf8_pragma? - @@handler - else - ActiveSupport::Multibyte::Handlers::PassthruHandler + # Works just like String#ljust, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".chars.rjust(8).to_s + # #=> "¾ cup " + # + # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace + # #=> "¾ cup " + def ljust(integer, padstr=' ') + justify(integer, :left, padstr) + end + + # Works just like String#center, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".chars.center(8).to_s + # #=> " ¾ cup " + # + # "¾ cup".chars.center(8, " ").to_s # Use non-breaking whitespace + # #=> " ¾ cup " + def center(integer, padstr=' ') + justify(integer, :center, padstr) end - end - private + # 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 - # +utf8_pragma+ checks if it can send this string to the handlers. It makes sure @string isn't nil and $KCODE is - # set to 'UTF8'. - def utf8_pragma? - !@string.nil? && ($KCODE == 'UTF8') + # Reverses all characters in the string + # + # Example: + # 'Café'.mb_chars.reverse.to_s #=> 'éfaC' + def reverse + chars(self.class.u_unpack(@wrapped_string).reverse.pack('U*')) end + + # Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that + # character. + # + # Example: + # 'こにちわ'.mb_chars.slice(2..3).to_s #=> "ちわ" + def slice(*args) + if args.size > 2 + raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native + elsif (args.size == 2 && !(args.first.is_a?(Numeric) || args.first.is_a?(Regexp))) + raise TypeError, "cannot convert #{args.first.class} into Integer" # Do as if we were native + 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) + 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]] + result = character.nil? ? nil : [character].pack('U') + else + result = self.class.u_unpack(@wrapped_string).slice(*args).pack('U*') + end + result.nil? ? nil : chars(result) + end + alias_method :[], :slice + + # Convert characters in the string to uppercase + # + # Example: + # 'Laurent, òu sont les tests?'.mb_chars.upcase.to_s #=> "LAURENT, ÒU SONT LES TESTS?" + def upcase + apply_mapping :uppercase_mapping + end + + # Convert characters in the string to lowercase + # + # Example: + # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s #=> "věda a výzkum" + def downcase + apply_mapping :lowercase_mapping + end + + # Converts the first character to uppercase and the remainder to lowercase + # + # Example: + # 'über'.mb_chars.capitalize.to_s #=> "Über" + def capitalize + (slice(0) || '').upcase + (slice(1..-1) || '').downcase + 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>str</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(form=ActiveSupport::Multibyte.default_normalization_form) + # See http://www.unicode.org/reports/tr15, Table 1 + codepoints = self.class.u_unpack(@wrapped_string) + chars(case form + when :d + self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints)) + when :c + self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints))) + when :kd + self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints)) + when :kc + self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints))) + else + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end.pack('U*')) + end + + # Performs canonical decomposition on all the characters. + # + # Example: + # 'é'.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*')) + end + + # Performs composition on all the characters. + # + # Example: + # 'é'.length #=> 3 + # 'é'.mb_chars.compose.to_s.length #=> 2 + def compose + chars(self.class.compose_codepoints(self.class.u_unpack(@wrapped_string)).pack('U*')) + end + + # Returns the number of grapheme clusters in the string. + # + # Example: + # 'क्षि'.mb_chars.length #=> 4 + # 'क्षि'.mb_chars.g_length #=> 3 + def g_length + self.class.g_unpack(@wrapped_string).length + end + + def tidy_bytes + chars(self.class.tidy_bytes(@wrapped_string)) + end + + %w(lstrip rstrip strip reverse upcase downcase slice tidy_bytes capitalize).each do |method| + define_method("#{method}!") do |*args| + unless args.nil? + @wrapped_string = send(method, *args).to_s + else + @wrapped_string = send(method).to_s + end + self + end + end + + class << self + + # Unpack the string at codepoints boundaries + def u_unpack(str) + begin + str.unpack 'U*' + rescue ArgumentError + raise EncodingError.new('malformed UTF-8 character') + end + end + + # Detect whether the codepoint is in a certain character class. 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 + def g_unpack(str) + codepoints = u_unpack(str) + 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 + def g_pack(unpacked) + (unpacked.flatten).pack('U*') + end + + # Generates a padding string of a certain size. + def padding(padsize, padstr=' ') + 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 + + # Replaces all the non-UTF-8 bytes by their iso-8859-1 or cp1252 equivalent resulting in a valid UTF-8 string + def tidy_bytes(str) + str.split(//u).map do |c| + if !UTF8_PAT.match(c) + n = c.unpack('C')[0] + n < 128 ? n.chr : + n < 160 ? [UCD.cp1252[n] || n].pack('U') : + n < 192 ? "\xC2" + n.chr : "\xC3" + (n-64).chr + else + c + end + end.join + end + end + + protected + + # Translate a byte offset in the wrapped string to a character offset by looking for the character boundary + def translate_offset(byte_offset) + return nil if byte_offset.nil? + return 0 if @wrapped_string == '' + chunk = @wrapped_string[0..byte_offset] + begin + begin + chunk.unpack('U*').length - 1 + rescue ArgumentError => e + chunk = @wrapped_string[0..(byte_offset+=1)] + # Stop retrying at the end of the string + raise e unless byte_offset < chunk.length + # We damaged a character, retry + retry + end + # Catch the ArgumentError so we can throw our own + rescue ArgumentError + raise EncodingError, 'malformed UTF-8 character' + end + end + + # Justifies a string in a certain way. Valid values for <tt>way</tt> are <tt>:right</tt>, <tt>:left</tt> and + # <tt>:center</tt>. + def justify(integer, way, padstr=' ') + raise ArgumentError, "zero width padding" if padstr.length == 0 + padsize = integer - size + padsize = padsize > 0 ? padsize : 0 + case way + when :right + result = @wrapped_string.dup.insert(0, self.class.padding(padsize, padstr)) + when :left + result = @wrapped_string.dup.insert(-1, self.class.padding(padsize, padstr)) + when :center + lpad = self.class.padding((padsize / 2.0).floor, padstr) + rpad = self.class.padding((padsize / 2.0).ceil, padstr) + result = @wrapped_string.dup.insert(0, lpad).insert(-1, rpad) + end + chars(result) + end + + # Map codepoints to one of it's attributes. + def apply_mapping(mapping) + 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*')) + end + + # Creates a new instance + def chars(str) + self.class.new(str) + end + end end -end - -# When we can load the utf8proc library, override normalization with the faster methods -begin - require 'utf8proc_native' - require 'active_support/multibyte/handlers/utf8_handler_proc' - ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8HandlerProc -rescue LoadError - ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8Handler -end +end \ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/exceptions.rb b/activesupport/lib/active_support/multibyte/exceptions.rb new file mode 100644 index 0000000000..af760cc561 --- /dev/null +++ b/activesupport/lib/active_support/multibyte/exceptions.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 + +module ActiveSupport #:nodoc: + module Multibyte #:nodoc: + class EncodingError < StandardError; end + end +end \ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/generators/generate_tables.rb b/activesupport/lib/active_support/multibyte/generators/generate_tables.rb deleted file mode 100755 index 7f807585c5..0000000000 --- a/activesupport/lib/active_support/multibyte/generators/generate_tables.rb +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env ruby -begin - require File.dirname(__FILE__) + '/../../../active_support' -rescue IOError -end -require 'open-uri' -require 'tmpdir' - -module ActiveSupport::Multibyte::Handlers #:nodoc: - class UnicodeDatabase #:nodoc: - def self.load - [Hash.new(Codepoint.new),[],{},{}] - end - end - - class UnicodeTableGenerator #:nodoc: - 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) - - @ucd.composition_exclusion = [] - @ucd.composition_map = {} - @ucd.boundary = {} - @ucd.cp1252 = {} - 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 - - def parse_composition_exclusion(line) - if line =~ /^([0-9A-F]+)/i - @ucd.composition_exclusion << $1.hex - end - end - - def parse_cp1252(line) - if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i - @ucd.cp1252[$1.hex] = $2.hex - 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[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 - - def normalize_boundary_map - @ucd.boundary.each do |k,v| - if [:lf, :cr].include? k - @ucd.boundary[k] = v[0] - 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 } - end - end - end - File.open(filename) do |file| - file.each_line { |line| send "parse_#{type}".intern, line } - end - 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]) - end - end - end -end - -if __FILE__ == $0 - filename = ActiveSupport::Multibyte::Handlers::UnicodeDatabase.filename - generator = ActiveSupport::Multibyte::Handlers::UnicodeTableGenerator.new - generator.parse - print "Writing to: #{filename}" - generator.dump_to filename - puts " (#{File.size(filename)} bytes)" -end diff --git a/activesupport/lib/active_support/multibyte/handlers/passthru_handler.rb b/activesupport/lib/active_support/multibyte/handlers/passthru_handler.rb deleted file mode 100644 index 916215c2ce..0000000000 --- a/activesupport/lib/active_support/multibyte/handlers/passthru_handler.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Chars uses this handler when $KCODE is not set to 'UTF8'. Because this handler doesn't define any methods all call -# will be forwarded to String. -class ActiveSupport::Multibyte::Handlers::PassthruHandler #:nodoc: - - # Return the original byteoffset - def self.translate_offset(string, byte_offset) #:nodoc: - byte_offset - end -end \ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/handlers/utf8_handler.rb b/activesupport/lib/active_support/multibyte/handlers/utf8_handler.rb deleted file mode 100644 index aa9c16f575..0000000000 --- a/activesupport/lib/active_support/multibyte/handlers/utf8_handler.rb +++ /dev/null @@ -1,564 +0,0 @@ -# Contains all the handlers and helper classes -module ActiveSupport::Multibyte::Handlers #:nodoc: - class EncodingError < ArgumentError #:nodoc: - end - - class Codepoint #:nodoc: - attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping - end - - class UnicodeDatabase #:nodoc: - attr_writer :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 - - # self-expiring methods that lazily load the Unicode database and then return the value. - [:codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252].each do |attr_name| - class_eval(<<-EOS, __FILE__, __LINE__) - def #{attr_name} - load - @#{attr_name} - end - EOS - end - - # Shortcut to ucd.codepoints[] - def [](index); codepoints[index]; 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 - - # Loads the unicode database and returns all the internal objects of UnicodeDatabase - # Once the values have been loaded, define attr_reader methods for the instance variables. - 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}), handler is unusable") - end - @codepoints ||= Hash.new(Codepoint.new) - @composition_exclusion ||= [] - @composition_map ||= {} - @boundary ||= {} - @cp1252 ||= {} - - # 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 :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 - end - end - end - - # UTF8Handler implements Unicode aware operations for strings, these operations will be used by the Chars - # proxy when $KCODE is set to 'UTF8'. - class UTF8Handler - # 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 - - # Borrowed from the Kconv library by Shinji KONO - (also as seen on the W3C site) - UTF8_PAT = /\A(?: - [\x00-\x7f] | - [\xc2-\xdf] [\x80-\xbf] | - \xe0 [\xa0-\xbf] [\x80-\xbf] | - [\xe1-\xef] [\x80-\xbf] [\x80-\xbf] | - \xf0 [\x90-\xbf] [\x80-\xbf] [\x80-\xbf] | - [\xf1-\xf3] [\x80-\xbf] [\x80-\xbf] [\x80-\xbf] | - \xf4 [\x80-\x8f] [\x80-\xbf] [\x80-\xbf] - )*\z/xn - - # 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/ - UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/ - - class << self - - # /// - # /// BEGIN String method overrides - # /// - - # Inserts the passed string at specified codepoint offsets - def insert(str, offset, fragment) - str.replace( - u_unpack(str).insert( - offset, - u_unpack(fragment) - ).flatten.pack('U*') - ) - end - - # Returns the position of the passed argument in the string, counting in codepoints - def index(str, *args) - bidx = str.index(*args) - bidx ? (u_unpack(str.slice(0...bidx)).size) : nil - end - - # Works just like the indexed replace method on string, except instead of byte offsets you specify - # character offsets. - # - # Example: - # - # s = "Müller" - # s.chars[2] = "e" # Replace character with offset 2 - # s # => "Müeler" - # - # s = "Müller" - # s.chars[1, 2] = "ö" # Replace 2 characters at character offset 1 - # s # => "Möler" - def []=(str, *args) - replace_by = args.pop - # Indexed replace with regular expressions already works - return str[*args] = replace_by if args.first.is_a?(Regexp) - result = u_unpack(str) - if args[0].is_a?(Fixnum) - raise IndexError, "index #{args[0]} out of string" if args[0] >= result.length - min = args[0] - max = args[1].nil? ? min : (min + args[1] - 1) - range = Range.new(min, max) - replace_by = [replace_by].pack('U') if replace_by.is_a?(Fixnum) - elsif args.first.is_a?(Range) - raise RangeError, "#{args[0]} out of range" if args[0].min >= result.length - range = args[0] - else - needle = args[0].to_s - min = index(str, needle) - max = min + length(needle) - 1 - range = Range.new(min, max) - end - result[range] = u_unpack(replace_by) - str.replace(result.pack('U*')) - end - - # Works just like String#rjust, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".chars.rjust(8).to_s - # # => " ¾ cup" - # - # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace - # # => " ¾ cup" - def rjust(str, integer, padstr=' ') - justify(str, integer, :right, padstr) - end - - # Works just like String#ljust, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".chars.rjust(8).to_s - # # => "¾ cup " - # - # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace - # # => "¾ cup " - def ljust(str, integer, padstr=' ') - justify(str, integer, :left, padstr) - end - - # Works just like String#center, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".chars.center(8).to_s - # # => " ¾ cup " - # - # "¾ cup".chars.center(8, " ").to_s # Use non-breaking whitespace - # # => " ¾ cup " - def center(str, integer, padstr=' ') - justify(str, integer, :center, padstr) - end - - # Does Unicode-aware rstrip - def rstrip(str) - str.gsub(UNICODE_TRAILERS_PAT, '') - end - - # Does Unicode-aware lstrip - def lstrip(str) - str.gsub(UNICODE_LEADERS_PAT, '') - end - - # Removed leading and trailing whitespace - def strip(str) - str.gsub(UNICODE_LEADERS_PAT, '').gsub(UNICODE_TRAILERS_PAT, '') - end - - # Returns the number of codepoints in the string - def size(str) - u_unpack(str).size - end - alias_method :length, :size - - # Reverses codepoints in the string. - def reverse(str) - u_unpack(str).reverse.pack('U*') - end - - # Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that - # character. - def slice(str, *args) - if args.size > 2 - raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native - elsif (args.size == 2 && !(args.first.is_a?(Numeric) || args.first.is_a?(Regexp))) - raise TypeError, "cannot convert #{args.first.class} into Integer" # Do as if we were native - 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 = u_unpack(str).slice(*args) - cps.nil? ? nil : cps.pack('U*') - elsif args[0].kind_of? Regexp - str.slice(*args) - elsif args.size == 1 && args[0].kind_of?(Numeric) - u_unpack(str)[args[0]] - else - u_unpack(str).slice(*args).pack('U*') - end - end - alias_method :[], :slice - - # Convert characters in the string to uppercase - def upcase(str); to_case :uppercase_mapping, str; end - - # Convert characters in the string to lowercase - def downcase(str); to_case :lowercase_mapping, str; end - - # Returns a copy of +str+ with the first character converted to uppercase and the remainder to lowercase - def capitalize(str) - upcase(slice(str, 0..0)) + downcase(slice(str, 1..-1) || '') - end - - # /// - # /// Extra String methods for unicode operations - # /// - - # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for - # passing strings to databases and validations. - # - # * <tt>str</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(str, form=ActiveSupport::Multibyte::DEFAULT_NORMALIZATION_FORM) - # See http://www.unicode.org/reports/tr15, Table 1 - codepoints = u_unpack(str) - 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 - - # Perform decomposition on the characters in the string - def decompose(str) - decompose_codepoints(:canonical, u_unpack(str)).pack('U*') - end - - # Perform composition on the characters in the string - def compose(str) - compose_codepoints u_unpack(str).pack('U*') - end - - # /// - # /// BEGIN Helper methods for unicode operation - # /// - - # Used to translate an offset from bytes to characters, for instance one received from a regular expression match - def translate_offset(str, byte_offset) - return nil if byte_offset.nil? - return 0 if str == '' - chunk = str[0..byte_offset] - begin - begin - chunk.unpack('U*').length - 1 - rescue ArgumentError => e - chunk = str[0..(byte_offset+=1)] - # Stop retrying at the end of the string - raise e unless byte_offset < chunk.length - # We damaged a character, retry - retry - end - # Catch the ArgumentError so we can throw our own - rescue ArgumentError - raise EncodingError.new('malformed UTF-8 character') - end - end - - # Checks if the string is valid UTF8. - def consumes?(str) - # Unpack is a little bit faster than regular expressions - begin - str.unpack('U*') - true - rescue ArgumentError - false - end - end - - # Returns the number of grapheme clusters in the string. This method is very likely to be moved or renamed - # in future versions. - def g_length(str) - g_unpack(str).length - end - - # Replaces all the non-utf-8 bytes by their iso-8859-1 or cp1252 equivalent resulting in a valid utf-8 string - def tidy_bytes(str) - str.split(//u).map do |c| - if !UTF8_PAT.match(c) - n = c.unpack('C')[0] - n < 128 ? n.chr : - n < 160 ? [UCD.cp1252[n] || n].pack('U') : - n < 192 ? "\xC2" + n.chr : "\xC3" + (n-64).chr - else - c - end - end.join - end - - protected - - # Detect whether the codepoint is in a certain character class. 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 codepoints boundaries - def u_unpack(str) - begin - str.unpack 'U*' - rescue ArgumentError - raise EncodingError.new('malformed UTF-8 character') - end - end - - # Unpack the string at grapheme boundaries instead of codepoint boundaries - def g_unpack(str) - codepoints = u_unpack(str) - 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 - def g_pack(unpacked) - unpacked.flatten - end - - # Justifies a string in a certain way. Valid values for <tt>way</tt> are <tt>:right</tt>, <tt>:left</tt> and - # <tt>:center</tt>. Is primarily used as a helper method by <tt>rjust</tt>, <tt>ljust</tt> and <tt>center</tt>. - def justify(str, integer, way, padstr=' ') - raise ArgumentError, "zero width padding" if padstr.length == 0 - padsize = integer - size(str) - padsize = padsize > 0 ? padsize : 0 - case way - when :right - str.dup.insert(0, padding(padsize, padstr)) - when :left - str.dup.insert(-1, padding(padsize, padstr)) - when :center - lpad = padding((padsize / 2.0).floor, padstr) - rpad = padding((padsize / 2.0).ceil, padstr) - str.dup.insert(0, lpad).insert(-1, rpad) - end - end - - # Generates a padding string of a certain size. - def padding(padsize, padstr=' ') - if padsize != 0 - slice(padstr * ((padsize / size(padstr)) + 1), 0, padsize) - else - '' - end - end - - # Convert characters to a different case - def to_case(way, str) - u_unpack(str).map do |codepoint| - cp = UCD[codepoint] - unless cp.nil? - ncp = cp.send(way) - ncp > 0 ? ncp : codepoint - else - codepoint - end - end.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 = UCD[codepoints[pos]], UCD[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[cp].decomp_mapping) and (!UCD[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[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 - - # UniCode Database - UCD = UnicodeDatabase.new - end - end -end diff --git a/activesupport/lib/active_support/multibyte/handlers/utf8_handler_proc.rb b/activesupport/lib/active_support/multibyte/handlers/utf8_handler_proc.rb deleted file mode 100644 index f10eecc622..0000000000 --- a/activesupport/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +++ /dev/null @@ -1,43 +0,0 @@ -# Methods in this handler call functions in the utf8proc ruby extension. These are significantly faster than the -# pure ruby versions. Chars automatically uses this handler when it can load the utf8proc extension. For -# documentation on handler methods see UTF8Handler. -class ActiveSupport::Multibyte::Handlers::UTF8HandlerProc < ActiveSupport::Multibyte::Handlers::UTF8Handler #:nodoc: - class << self - def normalize(str, form=ActiveSupport::Multibyte::DEFAULT_NORMALIZATION_FORM) #:nodoc: - codepoints = str.unpack('U*') - case form - when :d - utf8map(str, :stable) - when :c - utf8map(str, :stable, :compose) - when :kd - utf8map(str, :stable, :compat) - when :kc - utf8map(str, :stable, :compose, :compat) - else - raise ArgumentError, "#{form} is not a valid normalization variant", caller - end - end - - def decompose(str) #:nodoc: - utf8map(str, :stable) - end - - def downcase(str) #:nodoc:c - utf8map(str, :casefold) - end - - protected - - def utf8map(str, *option_array) #:nodoc: - options = 0 - option_array.each do |option| - flag = Utf8Proc::Options[option] - raise ArgumentError, "Unknown argument given to utf8map." unless - flag - options |= flag - end - return Utf8Proc::utf8map(str, options) - end - end -end diff --git a/activesupport/lib/active_support/multibyte/unicode_database.rb b/activesupport/lib/active_support/multibyte/unicode_database.rb new file mode 100644 index 0000000000..3b8cf8f9eb --- /dev/null +++ b/activesupport/lib/active_support/multibyte/unicode_database.rb @@ -0,0 +1,71 @@ +# 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__) + def #{attr_name} + load + @#{attr_name} + 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/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat index 35edb148c3..74b333d416 100644 Binary files a/activesupport/lib/active_support/values/unicode_tables.dat and b/activesupport/lib/active_support/values/unicode_tables.dat differ -- cgit v1.2.3 From 520c3f33c3f642ccab3a860cf5ee0b5530c7c4f1 Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:23:37 +0200 Subject: Change all calls to String#chars to String#mb_chars. --- .../lib/active_support/core_ext/string/access.rb | 10 +++++----- activesupport/lib/active_support/multibyte/chars.rb | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 1a949c0b77..7fb21fa4dd 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -11,7 +11,7 @@ module ActiveSupport #:nodoc: # "hello".at(4) # => "o" # "hello".at(10) # => nil def at(position) - chars[position, 1].to_s + mb_chars[position, 1].to_s end # Returns the remaining of the string from the +position+ treating the string as an array (where 0 is the first character). @@ -21,7 +21,7 @@ module ActiveSupport #:nodoc: # "hello".from(2) # => "llo" # "hello".from(10) # => nil def from(position) - chars[position..-1].to_s + mb_chars[position..-1].to_s end # Returns the beginning of the string up to the +position+ treating the string as an array (where 0 is the first character). @@ -31,7 +31,7 @@ module ActiveSupport #:nodoc: # "hello".to(2) # => "hel" # "hello".to(10) # => "hello" def to(position) - chars[0..position].to_s + mb_chars[0..position].to_s end # Returns the first character of the string or the first +limit+ characters. @@ -41,7 +41,7 @@ module ActiveSupport #:nodoc: # "hello".first(2) # => "he" # "hello".first(10) # => "hello" def first(limit = 1) - chars[0..(limit - 1)].to_s + mb_chars[0..(limit - 1)].to_s end # Returns the last character of the string or the last +limit+ characters. @@ -51,7 +51,7 @@ module ActiveSupport #:nodoc: # "hello".last(2) # => "lo" # "hello".last(10) # => "hello" def last(limit = 1) - (chars[(-limit)..-1] || self).to_s + (mb_chars[(-limit)..-1] || self).to_s end end else diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index c05419bfbf..cd0993d56b 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -9,12 +9,12 @@ module ActiveSupport #:nodoc: # String methods are proxied through the Chars object, and can be accessed through the +mb_chars+ method. Methods # which would normally return a String object now return a Chars object so methods can be chained. # - # "The Perfect String ".chars.downcase.strip.normalize #=> "the perfect string" + # "The Perfect String ".mb_chars.downcase.strip.normalize #=> "the perfect string" # # Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made. # If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them. # - # bad.explicit_checking_method "T".chars.downcase.to_s + # bad.explicit_checking_method "T".mb_chars.downcase.to_s # # The default Chars implementation assumes that the encoding of the string is UTF-8, if you want to handle different # encodings you can write your own multibyte string handler and configure it through @@ -213,12 +213,12 @@ module ActiveSupport #:nodoc: # Example: # # s = "Müller" - # s.chars[2] = "e" # Replace character with offset 2 + # s.mb_chars[2] = "e" # Replace character with offset 2 # s # #=> "Müeler" # # s = "Müller" - # s.chars[1, 2] = "ö" # Replace 2 characters at character offset 1 + # s.mb_chars[1, 2] = "ö" # Replace 2 characters at character offset 1 # s # #=> "Möler" def []=(*args) @@ -253,10 +253,10 @@ module ActiveSupport #:nodoc: # # Example: # - # "¾ cup".chars.rjust(8).to_s + # "¾ cup".mb_chars.rjust(8).to_s # #=> " ¾ cup" # - # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace + # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace # #=> " ¾ cup" def rjust(integer, padstr=' ') justify(integer, :right, padstr) @@ -266,10 +266,10 @@ module ActiveSupport #:nodoc: # # Example: # - # "¾ cup".chars.rjust(8).to_s + # "¾ cup".mb_chars.rjust(8).to_s # #=> "¾ cup " # - # "¾ cup".chars.rjust(8, " ").to_s # Use non-breaking whitespace + # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace # #=> "¾ cup " def ljust(integer, padstr=' ') justify(integer, :left, padstr) @@ -279,10 +279,10 @@ module ActiveSupport #:nodoc: # # Example: # - # "¾ cup".chars.center(8).to_s + # "¾ cup".mb_chars.center(8).to_s # #=> " ¾ cup " # - # "¾ cup".chars.center(8, " ").to_s # Use non-breaking whitespace + # "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace # #=> " ¾ cup " def center(integer, padstr=' ') justify(integer, :center, padstr) -- cgit v1.2.3 From 8abef4fd0df828e79be6b9fadd8f45c575ab817c Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:25:36 +0200 Subject: All methods which normally return a string now return a proxy instance. --- activesupport/lib/active_support/multibyte/chars.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index cd0993d56b..27cc3c65a2 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -246,7 +246,6 @@ module ActiveSupport #:nodoc: result[range] = self.class.u_unpack(replace_by) @wrapped_string.replace(result.pack('U*')) end - self end # Works just like String#rjust, only integer specifies characters instead of bytes. @@ -365,7 +364,7 @@ module ActiveSupport #:nodoc: # Example: # 'über'.mb_chars.capitalize.to_s #=> "Über" def capitalize - (slice(0) || '').upcase + (slice(1..-1) || '').downcase + (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase end # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for -- cgit v1.2.3 From bfc73852b1f03d8dee405cdeb0f2883d89c78b2d Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:28:05 +0200 Subject: Improve documentation. --- .../active_support/core_ext/string/multibyte.rb | 23 ++--- activesupport/lib/active_support/multibyte.rb | 4 +- .../lib/active_support/multibyte/chars.rb | 113 +++++++++++---------- .../lib/active_support/multibyte/exceptions.rb | 1 + 4 files changed, 74 insertions(+), 67 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 5a2dc36f72..3bf79bc7e1 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -6,7 +6,9 @@ module ActiveSupport #:nodoc: # Implements multibyte methods for easier access to multibyte characters in a String instance. module Multibyte unless '1.9'.respond_to?(:force_encoding) - # +mb_chars+ is a multibyte safe proxy method for string methods. + # == Multibyte proxy + # + # +mb_chars+ is a multibyte safe proxy for string methods. # # In Ruby 1.8 and older it creates and returns an instance of the ActiveSupport::Multibyte::Chars class which # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy @@ -19,11 +21,10 @@ module ActiveSupport #:nodoc: # name.mb_chars.reverse.to_s #=> "rellüM sualC" # name.mb_chars.length #=> 12 # - # In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware so we don't need - # a proxy class any more. This means that +mb_chars+ makes it easier to write code that runs on multiple Ruby - # versions. + # In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware. This means that + # it becomes easy to run one version of your code on multiple Ruby versions. # - # == Method chaining + # == Method chaining # # All the methods on the Chars proxy which normally return a string will return a Chars object. This allows # method chaining on the result of any of these methods. @@ -32,12 +33,12 @@ module ActiveSupport #:nodoc: # # == Interoperability and configuration # - # The Char object tries to be as interchangeable with String objects as possible: sorting and comparing between + # The Chars object tries to be as interchangeable with String objects as possible: sorting and comparing between # String and Char work like expected. The bang! methods change the internal string representation in the Chars # object. Interoperability problems can be resolved easily with a +to_s+ call. # # 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. + # information about how to change the default Multibyte behaviour see ActiveSupport::Multibyte. def mb_chars if ActiveSupport::Multibyte.proxy_class.wants?(self) ActiveSupport::Multibyte.proxy_class.new(self) @@ -56,15 +57,11 @@ module ActiveSupport #:nodoc: alias chars mb_chars end else - # In Ruby 1.9 and newer +mb_chars+ returns self. In Ruby 1.8 and older +mb_chars+ creates and returns an - # Unicode safe proxy for string operations, this makes it easier to write code that runs on multiple Ruby - # versions. - def mb_chars + def mb_chars #:nodoc self end - # Returns true if the string has valid UTF-8 encoding. - def is_utf8? + def is_utf8? #:nodoc case encoding when Encoding::UTF_8 valid_encoding? diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb index 63c0d50166..018aafe607 100644 --- a/activesupport/lib/active_support/multibyte.rb +++ b/activesupport/lib/active_support/multibyte.rb @@ -5,7 +5,7 @@ require 'active_support/multibyte/exceptions' require 'active_support/multibyte/unicode_database' module ActiveSupport #:nodoc: - module Multibyte #:nodoc: + module Multibyte # A list of all available normalization forms. See http://www.unicode.org/reports/tr15/tr15-29.html for more # information about normalization. NORMALIZATIONS_FORMS = [:c, :kc, :d, :kd] @@ -30,4 +30,4 @@ module ActiveSupport #:nodoc: mattr_accessor :proxy_class self.proxy_class = ActiveSupport::Multibyte::Chars end -end \ No newline at end of file +end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 27cc3c65a2..c61367968e 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -2,7 +2,7 @@ module ActiveSupport #:nodoc: module Multibyte #:nodoc: - # Chars enables you to work transparently with multibyte encodings in the Ruby String class without having extensive + # Chars enables you to work transparently with UTF-8 encoding in the Ruby String class without having extensive # knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an # encoding safe manner. All the normal String methods are also implemented on the proxy. # @@ -88,14 +88,14 @@ module ActiveSupport #:nodoc: alias to_s wrapped_string alias to_str wrapped_string - # Creates a new Chars instance. +string+ is the wrapped string. if '1.9'.respond_to?(:force_encoding) + # Creates a new Chars instance by wrapping _string_. def initialize(string) @wrapped_string = string @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen? end else - def initialize(string) + def initialize(string) #:nodoc: @wrapped_string = string end end @@ -121,10 +121,10 @@ module ActiveSupport #:nodoc: true end - # Returns +true+ if the Chars class can and should act as a proxy for the string +string+. Returns + # Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns # +false+ otherwise. def self.wants?(string) - RUBY_VERSION < '1.9' && $KCODE == 'UTF8' && consumes?(string) + $KCODE == 'UTF8' && consumes?(string) end # Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise. @@ -138,9 +138,9 @@ module ActiveSupport #:nodoc: include Comparable - # Returns -1, 0 or +1 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 String.<=> - # for more details. + # 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 @@ -148,7 +148,7 @@ module ActiveSupport #:nodoc: @wrapped_string <=> other.to_s end - # Returns a new Chars object containing the other object concatenated to the string. + # 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" @@ -156,7 +156,7 @@ module ActiveSupport #:nodoc: self << other end - # Like String.=~ only it returns the character offset (in codepoints) instead of the byte offset. + # 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 @@ -164,7 +164,7 @@ module ActiveSupport #:nodoc: translate_offset(@wrapped_string =~ other) end - # Works just like String#split, with the exception that the items in the resulting list are Chars + # 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: @@ -173,7 +173,7 @@ module ActiveSupport #:nodoc: @wrapped_string.split(*args).map { |i| i.mb_chars } end - # Inserts the passed string at specified codepoint offsets + # Inserts the passed string at specified codepoint offsets. # # Example: # 'Café'.mb_chars.insert(4, ' périferôl').to_s #=> "Café périferôl" @@ -189,7 +189,7 @@ module ActiveSupport #:nodoc: self end - # Returns true if contained string contains +other+. Returns false otherwise. + # Returns +true+ if contained string contains _other_. Returns +false+ otherwise. # # Example: # 'Café'.mb_chars.include?('é') #=> true @@ -198,17 +198,17 @@ module ActiveSupport #:nodoc: @wrapped_string.include?(other) end - # Returns the position of the passed argument in the string, counting in codepoints + # 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 - def index(*args) - index = @wrapped_string.index(*args) + # 'Café périferôl'.mb_chars.index(/\w/u) #=> 0 + def index(needle, offset=0) + index = @wrapped_string.index(needle, offset) index ? (self.class.u_unpack(@wrapped_string.slice(0...index)).size) : nil end - # Works just like the indexed replace method on string, except instead of byte offsets you specify - # character offsets. + # Like <tt>String#[]=</tt>, except instead of byte offsets you specify character offsets. # # Example: # @@ -248,7 +248,7 @@ module ActiveSupport #:nodoc: end end - # Works just like String#rjust, only integer specifies characters instead of bytes. + # Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes. # # Example: # @@ -261,7 +261,7 @@ module ActiveSupport #:nodoc: justify(integer, :right, padstr) end - # Works just like String#ljust, only integer specifies characters instead of bytes. + # Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes. # # Example: # @@ -274,7 +274,7 @@ module ActiveSupport #:nodoc: justify(integer, :left, padstr) end - # Works just like String#center, only integer specifies characters instead of bytes. + # Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes. # # Example: # @@ -308,7 +308,7 @@ module ActiveSupport #:nodoc: end alias_method :length, :size - # Reverses all characters in the string + # Reverses all characters in the string. # # Example: # 'Café'.mb_chars.reverse.to_s #=> 'éfaC' @@ -343,7 +343,7 @@ module ActiveSupport #:nodoc: end alias_method :[], :slice - # Convert characters in the string to uppercase + # Convert characters in the string to uppercase. # # Example: # 'Laurent, òu sont les tests?'.mb_chars.upcase.to_s #=> "LAURENT, ÒU SONT LES TESTS?" @@ -351,7 +351,7 @@ module ActiveSupport #:nodoc: apply_mapping :uppercase_mapping end - # Convert characters in the string to lowercase + # Convert characters in the string to lowercase. # # Example: # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s #=> "věda a výzkum" @@ -359,7 +359,7 @@ module ActiveSupport #:nodoc: apply_mapping :lowercase_mapping end - # Converts the first character to uppercase and the remainder to lowercase + # Converts the first character to uppercase and the remainder to lowercase. # # Example: # 'über'.mb_chars.capitalize.to_s #=> "Über" @@ -418,6 +418,7 @@ module ActiveSupport #:nodoc: self.class.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. def tidy_bytes chars(self.class.tidy_bytes(@wrapped_string)) end @@ -435,24 +436,35 @@ module ActiveSupport #:nodoc: class << self - # Unpack the string at codepoints boundaries - def u_unpack(str) + # 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 - str.unpack 'U*' + string.unpack 'U*' rescue ArgumentError raise EncodingError.new('malformed UTF-8 character') end end - # Detect whether the codepoint is in a certain character class. Primarily used by the - # grapheme cluster support. + # 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 - def g_unpack(str) - codepoints = u_unpack(str) + # 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 @@ -481,13 +493,15 @@ module ActiveSupport #:nodoc: unpacked end - # Reverse operation of g_unpack + # Reverse operation of g_unpack. + # + # Example: + # Chars.g_pack(Chars.g_unpack('क्षि')) #=> 'क्षि' def g_pack(unpacked) (unpacked.flatten).pack('U*') end - # Generates a padding string of a certain size. - def padding(padsize, padstr=' ') + def padding(padsize, padstr=' ') #:nodoc: if padsize != 0 new(padstr * ((padsize / u_unpack(padstr).size) + 1)).slice(0, padsize) else @@ -495,7 +509,7 @@ module ActiveSupport #:nodoc: end end - # Re-order codepoints so the string becomes canonical + # Re-order codepoints so the string becomes canonical. def reorder_characters(codepoints) length = codepoints.length- 1 pos = 0 @@ -511,7 +525,7 @@ module ActiveSupport #:nodoc: codepoints end - # Decompose composed characters to the decomposed form + # 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 @@ -532,7 +546,7 @@ module ActiveSupport #:nodoc: end end - # Compose decomposed characters to the composed form + # Compose decomposed characters to the composed form. def compose_codepoints(codepoints) pos = 0 eoa = codepoints.length - 1 @@ -591,9 +605,9 @@ module ActiveSupport #:nodoc: codepoints end - # Replaces all the non-UTF-8 bytes by their iso-8859-1 or cp1252 equivalent resulting in a valid UTF-8 string - def tidy_bytes(str) - str.split(//u).map do |c| + # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. + def tidy_bytes(string) + string.split(//u).map do |c| if !UTF8_PAT.match(c) n = c.unpack('C')[0] n < 128 ? n.chr : @@ -608,8 +622,7 @@ module ActiveSupport #:nodoc: protected - # Translate a byte offset in the wrapped string to a character offset by looking for the character boundary - def translate_offset(byte_offset) + def translate_offset(byte_offset) #:nodoc: return nil if byte_offset.nil? return 0 if @wrapped_string == '' chunk = @wrapped_string[0..byte_offset] @@ -629,9 +642,7 @@ module ActiveSupport #:nodoc: end end - # Justifies a string in a certain way. Valid values for <tt>way</tt> are <tt>:right</tt>, <tt>:left</tt> and - # <tt>:center</tt>. - def justify(integer, way, padstr=' ') + def justify(integer, way, padstr=' ') #:nodoc: raise ArgumentError, "zero width padding" if padstr.length == 0 padsize = integer - size padsize = padsize > 0 ? padsize : 0 @@ -648,8 +659,7 @@ module ActiveSupport #:nodoc: chars(result) end - # Map codepoints to one of it's attributes. - def apply_mapping(mapping) + 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 @@ -660,9 +670,8 @@ module ActiveSupport #:nodoc: end.pack('U*')) end - # Creates a new instance - def chars(str) - self.class.new(str) + def chars(string) #:nodoc: + self.class.new(string) end end end diff --git a/activesupport/lib/active_support/multibyte/exceptions.rb b/activesupport/lib/active_support/multibyte/exceptions.rb index af760cc561..62066e3c71 100644 --- a/activesupport/lib/active_support/multibyte/exceptions.rb +++ b/activesupport/lib/active_support/multibyte/exceptions.rb @@ -2,6 +2,7 @@ module ActiveSupport #:nodoc: module Multibyte #:nodoc: + # Raised when a problem with the encoding was found. class EncodingError < StandardError; end end end \ No newline at end of file -- cgit v1.2.3 From 809af7f5586cb3f2f913b21be168fbf72d58cbfe Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:29:22 +0200 Subject: Non-string results from forwarded methods should be returned vertabim. --- activesupport/lib/active_support/multibyte/chars.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index c61367968e..b3fdcdc650 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -106,10 +106,11 @@ module ActiveSupport #:nodoc: @wrapped_string.__send__(method, *args, &block) self else - chars(@wrapped_string.__send__(method, *args, &block)) + result = @wrapped_string.__send__(method, *args, &block) + result.kind_of?(String) ? chars(result) : result end end - + # Returns +true+ if _obj_ responds to the given method. Private methods are included in the search # only if the optional second parameter evaluates to +true+. def respond_to?(method, include_private=false) -- cgit v1.2.3 From 85c05b53948a64ab0e246239d18e01d317a74d7d Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:30:45 +0200 Subject: Add tests for u_unpack to make sure it raises an EncodingError on invalid UTF-8 strings. --- activesupport/lib/active_support/multibyte/chars.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index b3fdcdc650..5184026c63 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -446,7 +446,7 @@ module ActiveSupport #:nodoc: begin string.unpack 'U*' rescue ArgumentError - raise EncodingError.new('malformed UTF-8 character') + raise EncodingError, 'malformed UTF-8 character' end end -- cgit v1.2.3 From 44e44b42d9226c089f00970ced796c83f193f262 Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:47:19 +0200 Subject: Deprecated String#chars in favor of String#mb_chars. --- activesupport/lib/active_support/core_ext/string/multibyte.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 3bf79bc7e1..a4caa83b74 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -54,7 +54,10 @@ module ActiveSupport #:nodoc: end unless '1.8.7 and later'.respond_to?(:chars) - alias chars mb_chars + def chars + ActiveSupport::Deprecation.warn('String#chars has been deprecated in favor of String#mb_chars.', caller) + mb_chars + end end else def mb_chars #:nodoc -- cgit v1.2.3 From 00a428655149c349d29b9351dbc345a3b8025a58 Mon Sep 17 00:00:00 2001 From: Manfred Stienstra <manfred@fngtps.com> Date: Sun, 21 Sep 2008 17:51:01 +0200 Subject: Change call to String#chars in inflector to String#mb_chars. --- activesupport/lib/active_support/inflector.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 8a917a9eb2..b2046f26de 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -257,7 +257,7 @@ module ActiveSupport # <%= link_to(@person.name, person_path %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> def parameterize(string, sep = '-') - string.chars.normalize(:kd).to_s.gsub(/[^\x00-\x7F]+/, '').gsub(/[^a-z0-9_\-]+/i, sep).downcase + string.mb_chars.normalize(:kd).to_s.gsub(/[^\x00-\x7F]+/, '').gsub(/[^a-z0-9_\-]+/i, sep).downcase end # Create the name of a table like Rails does for models to table names. This method -- cgit v1.2.3 From 5f86451a4c5d0beca5a746c4708be48b13f665be Mon Sep 17 00:00:00 2001 From: Michael Koziarski <michael@koziarski.com> Date: Mon, 22 Sep 2008 17:14:54 +0200 Subject: Bump the Version constants to align with the *next* release rather than the previous release. This allows people tracking non-release gems or git submodules to use the constants. --- activesupport/lib/active_support/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index b346459a9b..8f5395fca6 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,7 +1,7 @@ module ActiveSupport module VERSION #:nodoc: MAJOR = 2 - MINOR = 1 + MINOR = 2 TINY = 0 STRING = [MAJOR, MINOR, TINY].join('.') -- cgit v1.2.3 From a4f2ba8fb3c46ef9f7e31725849efdcb1a22c72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cig=C3=A1nek?= <adam.ciganek@gmail.com> Date: Fri, 12 Sep 2008 14:45:11 +0200 Subject: Modified ActiveSupport::Inflector#parameterize with code from slugalizer (http://github.com/henrik/slugalizer) Handles trailing and leading slashes, and squashes repeated separators into a single character. Signed-off-by: Michael Koziarski <michael@koziarski.com> [#1034 state:committed] --- activesupport/lib/active_support/inflector.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index b2046f26de..89a93f4a5f 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -240,9 +240,9 @@ module ActiveSupport def demodulize(class_name_in_module) class_name_in_module.to_s.gsub(/^.*::/, '') end - + # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. - # + # # ==== Examples # # class Person @@ -250,14 +250,20 @@ module ActiveSupport # "#{id}-#{name.parameterize}" # end # end - # + # # @person = Person.find(1) # # => #<Person id: 1, name: "Donald E. Knuth"> - # + # # <%= link_to(@person.name, person_path %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> def parameterize(string, sep = '-') - string.mb_chars.normalize(:kd).to_s.gsub(/[^\x00-\x7F]+/, '').gsub(/[^a-z0-9_\-]+/i, sep).downcase + re_sep = Regexp.escape(sep) + string.mb_chars.normalize(:kd). # Decompose accented characters + gsub(/[^\x00-\x7F]+/, ''). # Remove anything non-ASCII entirely (e.g. diacritics). + gsub(/[^a-z0-9\-_\+]+/i, sep). # Turn unwanted chars into the separator. + squeeze(sep). # No more than one of the separator in a row. + gsub(/^#{re_sep}|#{re_sep}$/i, ''). # Remove leading/trailing separator. + downcase end # Create the name of a table like Rails does for models to table names. This method -- cgit v1.2.3 From 2e75bd0808f4dcac328b690aaad176cbfe96773e Mon Sep 17 00:00:00 2001 From: adam <adam@3scale.net> Date: Tue, 23 Sep 2008 12:08:24 +0200 Subject: slice now returns indifferent hash if called on one Signed-off-by: Michael Koziarski <michael@koziarski.com> [#1096 state:committed] --- activesupport/lib/active_support/core_ext/hash/slice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport/lib') diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 3f14470f36..88df49a69f 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -18,7 +18,7 @@ module ActiveSupport #:nodoc: # Returns a new hash with only the given keys. def slice(*keys) keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key) - hash = {} + hash = self.class.new keys.each { |k| hash[k] = self[k] if has_key?(k) } hash end -- cgit v1.2.3