activesupport 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (55) hide show
  1. data/CHANGELOG +232 -2
  2. data/README +43 -0
  3. data/lib/active_support.rb +4 -1
  4. data/lib/active_support/breakpoint.rb +5 -0
  5. data/lib/active_support/core_ext/array.rb +2 -16
  6. data/lib/active_support/core_ext/array/conversions.rb +30 -4
  7. data/lib/active_support/core_ext/array/grouping.rb +55 -0
  8. data/lib/active_support/core_ext/bigdecimal.rb +3 -0
  9. data/lib/active_support/core_ext/bigdecimal/formatting.rb +7 -0
  10. data/lib/active_support/core_ext/class/inheritable_attributes.rb +6 -1
  11. data/lib/active_support/core_ext/date/conversions.rb +13 -7
  12. data/lib/active_support/core_ext/enumerable.rb +41 -10
  13. data/lib/active_support/core_ext/exception.rb +2 -2
  14. data/lib/active_support/core_ext/hash/conversions.rb +123 -12
  15. data/lib/active_support/core_ext/hash/indifferent_access.rb +18 -9
  16. data/lib/active_support/core_ext/integer/inflections.rb +10 -4
  17. data/lib/active_support/core_ext/load_error.rb +3 -3
  18. data/lib/active_support/core_ext/module.rb +2 -0
  19. data/lib/active_support/core_ext/module/aliasing.rb +58 -0
  20. data/lib/active_support/core_ext/module/attr_internal.rb +31 -0
  21. data/lib/active_support/core_ext/module/delegation.rb +27 -2
  22. data/lib/active_support/core_ext/name_error.rb +20 -0
  23. data/lib/active_support/core_ext/string.rb +2 -0
  24. data/lib/active_support/core_ext/string/access.rb +5 -5
  25. data/lib/active_support/core_ext/string/inflections.rb +93 -4
  26. data/lib/active_support/core_ext/string/unicode.rb +42 -0
  27. data/lib/active_support/core_ext/symbol.rb +1 -1
  28. data/lib/active_support/core_ext/time/calculations.rb +7 -5
  29. data/lib/active_support/core_ext/time/conversions.rb +1 -2
  30. data/lib/active_support/dependencies.rb +417 -50
  31. data/lib/active_support/deprecation.rb +201 -0
  32. data/lib/active_support/inflections.rb +1 -2
  33. data/lib/active_support/inflector.rb +117 -19
  34. data/lib/active_support/json.rb +14 -3
  35. data/lib/active_support/json/encoders/core.rb +21 -18
  36. data/lib/active_support/multibyte.rb +7 -0
  37. data/lib/active_support/multibyte/chars.rb +129 -0
  38. data/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
  39. data/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
  40. data/lib/active_support/multibyte/handlers/utf8_handler.rb +453 -0
  41. data/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +44 -0
  42. data/lib/active_support/option_merger.rb +3 -3
  43. data/lib/active_support/ordered_options.rb +24 -23
  44. data/lib/active_support/reloadable.rb +39 -5
  45. data/lib/active_support/values/time_zone.rb +1 -1
  46. data/lib/active_support/values/unicode_tables.dat +0 -0
  47. data/lib/active_support/vendor/builder/blankslate.rb +16 -6
  48. data/lib/active_support/vendor/builder/xchar.rb +112 -0
  49. data/lib/active_support/vendor/builder/xmlbase.rb +12 -10
  50. data/lib/active_support/vendor/builder/xmlmarkup.rb +26 -7
  51. data/lib/active_support/vendor/xml_simple.rb +1021 -0
  52. data/lib/active_support/version.rb +2 -2
  53. data/lib/active_support/whiny_nil.rb +1 -1
  54. metadata +26 -4
  55. data/lib/active_support/core_ext/hash/conversions.rb.rej +0 -28
@@ -4,14 +4,20 @@ module ActiveSupport
4
4
  module JSON #:nodoc:
5
5
  class CircularReferenceError < StandardError #:nodoc:
6
6
  end
7
- # returns the literal string as its JSON encoded form. Useful for passing javascript variables into functions.
8
- #
9
- # page.call 'Element.show', ActiveSupport::JSON::Variable.new("$$(#items li)")
7
+
8
+ # A string that returns itself as as its JSON-encoded form.
10
9
  class Variable < String #:nodoc:
11
10
  def to_json
12
11
  self
13
12
  end
14
13
  end
14
+
15
+ # When +true+, Hash#to_json will omit quoting string or symbol keys
16
+ # if the keys are valid JavaScript identifiers. Note that this is
17
+ # technically improper JSON (all object keys must be quoted), so if
18
+ # you need strict JSON compliance, set this option to +false+.
19
+ mattr_accessor :unquote_hash_key_identifiers
20
+ @@unquote_hash_key_identifiers = true
15
21
 
16
22
  class << self
17
23
  REFERENCE_STACK_VARIABLE = :json_reference_stack
@@ -22,6 +28,11 @@ module ActiveSupport
22
28
  end
23
29
  end
24
30
 
31
+ def can_unquote_identifier?(key)
32
+ return false unless unquote_hash_key_identifiers
33
+ key.to_s =~ /^[[:alpha:]_$][[:alnum:]_$]*$/
34
+ end
35
+
25
36
  protected
26
37
  def raise_on_circular_reference(value)
27
38
  stack = Thread.current[REFERENCE_STACK_VARIABLE] ||= []
@@ -16,24 +16,25 @@ module ActiveSupport
16
16
  define_encoder NilClass do
17
17
  'null'
18
18
  end
19
+
20
+ ESCAPED_CHARS = {
21
+ "\010" => '\b',
22
+ "\f" => '\f',
23
+ "\n" => '\n',
24
+ "\r" => '\r',
25
+ "\t" => '\t',
26
+ '"' => '\"',
27
+ '\\' => '\\\\'
28
+ }
19
29
 
20
30
  define_encoder String do |string|
21
- returning value = '"' do
22
- string.each_char do |char|
23
- value << case
24
- when char == "\010": '\b'
25
- when char == "\f": '\f'
26
- when char == "\n": '\n'
27
- when char == "\r": '\r'
28
- when char == "\t": '\t'
29
- when char == '"': '\"'
30
- when char == '\\': '\\\\'
31
- when char.length > 1: "\\u#{'%04x' % char.unpack('U').first}"
32
- else; char
33
- end
34
- end
35
- value << '"'
36
- end
31
+ '"' + string.gsub(/[\010\f\n\r\t"\\]/) { |s|
32
+ ESCAPED_CHARS[s]
33
+ }.gsub(/([\xC0-\xDF][\x80-\xBF]|
34
+ [\xE0-\xEF][\x80-\xBF]{2}|
35
+ [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s|
36
+ s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/, '\\\\u\&')
37
+ } + '"'
37
38
  end
38
39
 
39
40
  define_encoder Numeric do |numeric|
@@ -50,8 +51,10 @@ module ActiveSupport
50
51
 
51
52
  define_encoder Hash do |hash|
52
53
  returning result = '{' do
53
- result << hash.map do |pair|
54
- pair.map { |value| value.to_json } * ': '
54
+ result << hash.map do |key, value|
55
+ key = ActiveSupport::JSON::Variable.new(key.to_s) if
56
+ ActiveSupport::JSON.can_unquote_identifier?(key)
57
+ "#{key.to_json}: #{value.to_json}"
55
58
  end * ', '
56
59
  result << '}'
57
60
  end
@@ -0,0 +1,7 @@
1
+ module ActiveSupport::Multibyte
2
+ DEFAULT_NORMALIZATION_FORM = :kc
3
+ NORMALIZATIONS_FORMS = [:c, :kc, :d, :kd]
4
+ UNICODE_VERSION = '5.0.0'
5
+ end
6
+
7
+ require 'active_support/multibyte/chars'
@@ -0,0 +1,129 @@
1
+ require 'active_support/multibyte/handlers/utf8_handler'
2
+ require 'active_support/multibyte/handlers/passthru_handler'
3
+
4
+ # Encapsulates all the functionality related to the Chars proxy.
5
+ module ActiveSupport::Multibyte
6
+ # Chars enables you to work transparently with multibyte encodings in the Ruby String class without having extensive
7
+ # knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an
8
+ # encoding safe manner. All the normal String methods are also implemented on the proxy.
9
+ #
10
+ # String methods are proxied through the Chars object, and can be accessed through the +chars+ method. Methods
11
+ # which would normally return a String object now return a Chars object so methods can be chained.
12
+ #
13
+ # "The Perfect String ".chars.downcase.strip.normalize #=> "the perfect string"
14
+ #
15
+ # Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made.
16
+ # If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them.
17
+ #
18
+ # bad.explicit_checking_method "T".chars.downcase.to_s
19
+ #
20
+ # The actual operations on the string are delegated to handlers. Theoretically handlers can be implemented for
21
+ # any encoding, but the default handler handles UTF-8. This handler is set during initialization, if you want to
22
+ # use you own handler, you can set it on the Chars class. Look at the UTF8Handler source for an example how to
23
+ # implement your own handler. If you your own handler to work on anything but UTF-8 you probably also
24
+ # want to override Chars#handler.
25
+ #
26
+ # ActiveSupport::Multibyte::Chars.handler = MyHandler
27
+ #
28
+ # Note that a few methods are defined on Chars instead of the handler because they are defined on Object or Kernel
29
+ # and method_missing can't catch them.
30
+ class Chars
31
+
32
+ attr_reader :string # The contained string
33
+ alias_method :to_s, :string
34
+
35
+ include Comparable
36
+
37
+ # The magic method to make String and Chars comparable
38
+ def to_str
39
+ # Using any other ways of overriding the String itself will lead you all the way from infinite loops to
40
+ # core dumps. Don't go there.
41
+ @string
42
+ end
43
+
44
+ # Create a new Chars instance.
45
+ def initialize(str)
46
+ @string = (str.string rescue str)
47
+ end
48
+
49
+ # Returns -1, 0 or +1 depending on whether the Chars object is to be sorted before, equal or after the
50
+ # object on the right side of the operation. It accepts any object that implements +to_s+. See String.<=>
51
+ # for more details.
52
+ def <=>(other); @string <=> other.to_s; end
53
+
54
+ # Works just like String#split, with the exception that the items in the resulting list are Chars
55
+ # instances instead of String. This makes chaining methods easier.
56
+ def split(*args)
57
+ @string.split(*args).map { |i| i.chars }
58
+ end
59
+
60
+ # Gsub works exactly the same as gsub on a normal string.
61
+ def gsub(*a, &b); @string.gsub(*a, &b).chars; end
62
+
63
+ # Like String.=~ only it returns the character offset (in codepoints) instead of the byte offset.
64
+ def =~(other)
65
+ handler.translate_offset(@string, @string =~ other)
66
+ end
67
+
68
+ # Try to forward all undefined methods to the handler, when a method is not defined on the handler, send it to
69
+ # the contained string. Method_missing is also responsible for making the bang! methods destructive.
70
+ def method_missing(m, *a, &b)
71
+ begin
72
+ # Simulate methods with a ! at the end because we can't touch the enclosed string from the handlers.
73
+ if m.to_s =~ /^(.*)\!$/
74
+ result = handler.send($1, @string, *a, &b)
75
+ if result == @string
76
+ result = nil
77
+ else
78
+ @string.replace result
79
+ end
80
+ else
81
+ result = handler.send(m, @string, *a, &b)
82
+ end
83
+ rescue NoMethodError
84
+ result = @string.send(m, *a, &b)
85
+ rescue Handlers::EncodingError
86
+ @string.replace handler.tidy_bytes(@string)
87
+ retry
88
+ end
89
+
90
+ if result.kind_of?(String)
91
+ result.chars
92
+ else
93
+ result
94
+ end
95
+ end
96
+
97
+ # Set the handler class for the Char objects.
98
+ def self.handler=(klass)
99
+ @@handler = klass
100
+ end
101
+
102
+ # Returns the proper handler for the contained string depending on $KCODE and the encoding of the string. This
103
+ # method is used internally to always redirect messages to the proper classes depending on the context.
104
+ def handler
105
+ if utf8_pragma?
106
+ @@handler
107
+ else
108
+ ActiveSupport::Multibyte::Handlers::PassthruHandler
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ # +utf8_pragma+ checks if it can send this string to the handlers. It makes sure @string isn't nil and $KCODE is
115
+ # set to 'UTF8'.
116
+ def utf8_pragma?
117
+ !@string.nil? && ($KCODE == 'UTF8')
118
+ end
119
+ end
120
+ end
121
+
122
+ # When we can load the utf8proc library, override normalization with the faster methods
123
+ begin
124
+ require 'utf8proc_native'
125
+ require 'active_support/multibyte/handlers/utf8_handler_proc'
126
+ ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8HandlerProc
127
+ rescue LoadError
128
+ ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8Handler
129
+ end
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require File.dirname(__FILE__) + '/../../../active_support'
4
+ rescue IOError
5
+ end
6
+ require 'open-uri'
7
+ require 'tmpdir'
8
+
9
+ module ActiveSupport::Multibyte::Handlers #:nodoc:
10
+ class UnicodeDatabase #:nodoc:
11
+ def self.load
12
+ [Hash.new(Codepoint.new),[],{},{}]
13
+ end
14
+ end
15
+
16
+ class UnicodeTableGenerator #:nodoc:
17
+ BASE_URI = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::UNICODE_VERSION}/ucd/"
18
+ SOURCES = {
19
+ :codepoints => BASE_URI + 'UnicodeData.txt',
20
+ :composition_exclusion => BASE_URI + 'CompositionExclusions.txt',
21
+ :grapheme_break_property => BASE_URI + 'auxiliary/GraphemeBreakProperty.txt',
22
+ :cp1252 => 'http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT'
23
+ }
24
+
25
+ def initialize
26
+ @ucd = UnicodeDatabase.new
27
+
28
+ default = Codepoint.new
29
+ default.combining_class = 0
30
+ default.uppercase_mapping = 0
31
+ default.lowercase_mapping = 0
32
+ @ucd.codepoints = Hash.new(default)
33
+
34
+ @ucd.composition_exclusion = []
35
+ @ucd.composition_map = {}
36
+ @ucd.boundary = {}
37
+ @ucd.cp1252 = {}
38
+ end
39
+
40
+ def parse_codepoints(line)
41
+ codepoint = Codepoint.new
42
+ raise "Could not parse input." unless line =~ /^
43
+ ([0-9A-F]+); # code
44
+ ([^;]+); # name
45
+ ([A-Z]+); # general category
46
+ ([0-9]+); # canonical combining class
47
+ ([A-Z]+); # bidi class
48
+ (<([A-Z]*)>)? # decomposition type
49
+ ((\ ?[0-9A-F]+)*); # decompomposition mapping
50
+ ([0-9]*); # decimal digit
51
+ ([0-9]*); # digit
52
+ ([^;]*); # numeric
53
+ ([YN]*); # bidi mirrored
54
+ ([^;]*); # unicode 1.0 name
55
+ ([^;]*); # iso comment
56
+ ([0-9A-F]*); # simple uppercase mapping
57
+ ([0-9A-F]*); # simple lowercase mapping
58
+ ([0-9A-F]*)$/ix # simple titlecase mapping
59
+ codepoint.code = $1.hex
60
+ #codepoint.name = $2
61
+ #codepoint.category = $3
62
+ codepoint.combining_class = Integer($4)
63
+ #codepoint.bidi_class = $5
64
+ codepoint.decomp_type = $7
65
+ codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex }
66
+ #codepoint.bidi_mirrored = ($13=='Y') ? true : false
67
+ codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex
68
+ codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex
69
+ #codepoint.titlecase_mapping = ($18=='') ? nil : $18.hex
70
+ @ucd.codepoints[codepoint.code] = codepoint
71
+ end
72
+
73
+ def parse_grapheme_break_property(line)
74
+ if line =~ /^([0-9A-F\.]+)\s*;\s*([\w]+)\s*#/
75
+ type = $2.downcase.intern
76
+ @ucd.boundary[type] ||= []
77
+ if $1.include? '..'
78
+ parts = $1.split '..'
79
+ @ucd.boundary[type] << (parts[0].hex..parts[1].hex)
80
+ else
81
+ @ucd.boundary[type] << $1.hex
82
+ end
83
+ end
84
+ end
85
+
86
+ def parse_composition_exclusion(line)
87
+ if line =~ /^([0-9A-F]+)/i
88
+ @ucd.composition_exclusion << $1.hex
89
+ end
90
+ end
91
+
92
+ def parse_cp1252(line)
93
+ if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i
94
+ @ucd.cp1252[$1.hex] = $2.hex
95
+ end
96
+ end
97
+
98
+ def create_composition_map
99
+ @ucd.codepoints.each do |_, cp|
100
+ 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)
101
+ @ucd.composition_map[cp.decomp_mapping[0]] ||= {}
102
+ @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code
103
+ end
104
+ end
105
+ end
106
+
107
+ def normalize_boundary_map
108
+ @ucd.boundary.each do |k,v|
109
+ if [:lf, :cr].include? k
110
+ @ucd.boundary[k] = v[0]
111
+ end
112
+ end
113
+ end
114
+
115
+ def parse
116
+ SOURCES.each do |type, url|
117
+ filename = File.join(Dir.tmpdir, "#{url.split('/').last}")
118
+ unless File.exist?(filename)
119
+ $stderr.puts "Downloading #{url.split('/').last}"
120
+ File.open(filename, 'wb') do |target|
121
+ open(url) do |source|
122
+ source.each_line { |line| target.write line }
123
+ end
124
+ end
125
+ end
126
+ File.open(filename) do |file|
127
+ file.each_line { |line| send "parse_#{type}".intern, line }
128
+ end
129
+ end
130
+ create_composition_map
131
+ normalize_boundary_map
132
+ end
133
+
134
+ def dump_to(filename)
135
+ File.open(filename, 'wb') do |f|
136
+ f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252])
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ if __FILE__ == $0
143
+ filename = ActiveSupport::Multibyte::Handlers::UnicodeDatabase.filename
144
+ generator = ActiveSupport::Multibyte::Handlers::UnicodeTableGenerator.new
145
+ generator.parse
146
+ print "Writing to: #{filename}"
147
+ generator.dump_to filename
148
+ puts " (#{File.size(filename)} bytes)"
149
+ end
@@ -0,0 +1,9 @@
1
+ # Chars uses this handler when $KCODE is not set to 'UTF8'. Because this handler doesn't define any methods all call
2
+ # will be forwarded to String.
3
+ class ActiveSupport::Multibyte::Handlers::PassthruHandler
4
+
5
+ # Return the original byteoffset
6
+ def self.translate_offset(string, byte_offset) #:nodoc:
7
+ byte_offset
8
+ end
9
+ end
@@ -0,0 +1,453 @@
1
+ # Contains all the handlers and helper classes
2
+ module ActiveSupport::Multibyte::Handlers
3
+ class EncodingError < ArgumentError; end
4
+
5
+ class Codepoint #:nodoc:
6
+ attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
7
+ end
8
+
9
+ class UnicodeDatabase #:nodoc:
10
+ attr_writer :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
11
+
12
+ # self-expiring methods that lazily load the Unicode database and then return the value.
13
+ [:codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252].each do |attr_name|
14
+ class_eval(<<-EOS, __FILE__, __LINE__)
15
+ def #{attr_name}
16
+ load
17
+ @#{attr_name}
18
+ end
19
+ EOS
20
+ end
21
+
22
+ # Shortcut to ucd.codepoints[]
23
+ def [](index); codepoints[index]; end
24
+
25
+ # Returns the directory in which the data files are stored
26
+ def self.dirname
27
+ File.dirname(__FILE__) + '/../../values/'
28
+ end
29
+
30
+ # Returns the filename for the data file for this version
31
+ def self.filename
32
+ File.expand_path File.join(dirname, "unicode_tables.dat")
33
+ end
34
+
35
+ # Loads the unicode database and returns all the internal objects of UnicodeDatabase
36
+ # Once the values have been loaded, define attr_reader methods for the instance variables.
37
+ def load
38
+ begin
39
+ @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
40
+ rescue Exception => e
41
+ raise IOError.new("Couldn't load the unicode tables for UTF8Handler (#{e.message}), handler is unusable")
42
+ end
43
+ @codepoints ||= Hash.new(Codepoint.new)
44
+ @composition_exclusion ||= []
45
+ @composition_map ||= {}
46
+ @boundary ||= {}
47
+ @cp1252 ||= {}
48
+
49
+ # Redefine the === method so we can write shorter rules for grapheme cluster breaks
50
+ @boundary.each do |k,_|
51
+ @boundary[k].instance_eval do
52
+ def ===(other)
53
+ detect { |i| i === other } ? true : false
54
+ end
55
+ end if @boundary[k].kind_of?(Array)
56
+ end
57
+
58
+ # define attr_reader methods for the instance variables
59
+ class << self
60
+ attr_reader :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
61
+ end
62
+ end
63
+ end
64
+
65
+ # UTF8Handler implements Unicode aware operations for strings, these operations will be used by the Chars
66
+ # proxy when $KCODE is set to 'UTF8'.
67
+ class UTF8Handler
68
+ # Hangul character boundaries and properties
69
+ HANGUL_SBASE = 0xAC00
70
+ HANGUL_LBASE = 0x1100
71
+ HANGUL_VBASE = 0x1161
72
+ HANGUL_TBASE = 0x11A7
73
+ HANGUL_LCOUNT = 19
74
+ HANGUL_VCOUNT = 21
75
+ HANGUL_TCOUNT = 28
76
+ HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
77
+ HANGUL_SCOUNT = 11172
78
+ HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
79
+ HANGUL_JAMO_FIRST = 0x1100
80
+ HANGUL_JAMO_LAST = 0x11FF
81
+
82
+ # All the unicode whitespace
83
+ UNICODE_WHITESPACE = [
84
+ (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
85
+ 0x0020, # White_Space # Zs SPACE
86
+ 0x0085, # White_Space # Cc <control-0085>
87
+ 0x00A0, # White_Space # Zs NO-BREAK SPACE
88
+ 0x1680, # White_Space # Zs OGHAM SPACE MARK
89
+ 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
90
+ (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
91
+ 0x2028, # White_Space # Zl LINE SEPARATOR
92
+ 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
93
+ 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
94
+ 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
95
+ 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
96
+ ].flatten.freeze
97
+
98
+ # BOM (byte order mark) can also be seen as whitespace, it's a non-rendering character used to distinguish
99
+ # between little and big endian. This is not an issue in utf-8, so it must be ignored.
100
+ UNICODE_LEADERS_AND_TRAILERS = UNICODE_WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM
101
+
102
+ # Borrowed from the Kconv library by Shinji KONO - (also as seen on the W3C site)
103
+ UTF8_PAT = /\A(?:
104
+ [\x00-\x7f] |
105
+ [\xc2-\xdf] [\x80-\xbf] |
106
+ \xe0 [\xa0-\xbf] [\x80-\xbf] |
107
+ [\xe1-\xef] [\x80-\xbf] [\x80-\xbf] |
108
+ \xf0 [\x90-\xbf] [\x80-\xbf] [\x80-\xbf] |
109
+ [\xf1-\xf3] [\x80-\xbf] [\x80-\xbf] [\x80-\xbf] |
110
+ \xf4 [\x80-\x8f] [\x80-\xbf] [\x80-\xbf]
111
+ )*\z/xn
112
+
113
+ # Returns a regular expression pattern that matches the passed Unicode codepoints
114
+ def self.codepoints_to_pattern(array_of_codepoints) #:nodoc:
115
+ array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|')
116
+ end
117
+ UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/
118
+ UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/
119
+
120
+ class << self
121
+
122
+ # ///
123
+ # /// BEGIN String method overrides
124
+ # ///
125
+
126
+ # Inserts the passed string at specified codepoint offsets
127
+ def insert(str, offset, fragment)
128
+ str.replace(
129
+ u_unpack(str).insert(
130
+ offset,
131
+ u_unpack(fragment)
132
+ ).flatten.pack('U*')
133
+ )
134
+ end
135
+
136
+ # Returns the position of the passed argument in the string, counting in codepoints
137
+ def index(str, *args)
138
+ bidx = str.index(*args)
139
+ bidx ? (u_unpack(str.slice(0...bidx)).size) : nil
140
+ end
141
+
142
+ # Does Unicode-aware rstrip
143
+ def rstrip(str)
144
+ str.gsub(UNICODE_TRAILERS_PAT, '')
145
+ end
146
+
147
+ # Does Unicode-aware lstrip
148
+ def lstrip(str)
149
+ str.gsub(UNICODE_LEADERS_PAT, '')
150
+ end
151
+
152
+ # Removed leading and trailing whitespace
153
+ def strip(str)
154
+ str.gsub(UNICODE_LEADERS_PAT, '').gsub(UNICODE_TRAILERS_PAT, '')
155
+ end
156
+
157
+ # Returns the number of codepoints in the string
158
+ def size(str)
159
+ u_unpack(str).size
160
+ end
161
+ alias_method :length, :size
162
+
163
+ # Reverses codepoints in the string.
164
+ def reverse(str)
165
+ u_unpack(str).reverse.pack('U*')
166
+ end
167
+
168
+ # Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that
169
+ # character.
170
+ def slice(str, *args)
171
+ if (args.size == 2 && args.first.is_a?(Range))
172
+ raise TypeError, 'cannot convert Range into Integer' # Do as if we were native
173
+ elsif args[0].kind_of? Range
174
+ cps = u_unpack(str).slice(*args)
175
+ cps.nil? ? nil : cps.pack('U*')
176
+ elsif args.size == 1 && args[0].kind_of?(Numeric)
177
+ u_unpack(str)[args[0]]
178
+ else
179
+ u_unpack(str).slice(*args).pack('U*')
180
+ end
181
+ end
182
+ alias_method :[], :slice
183
+
184
+ # Convert characters in the string to uppercase
185
+ def upcase(str); to_case :uppercase_mapping, str; end
186
+
187
+ # Convert characters in the string to lowercase
188
+ def downcase(str); to_case :lowercase_mapping, str; end
189
+
190
+ # Returns a copy of +str+ with the first character converted to uppercase and the remainder to lowercase
191
+ def capitalize(str)
192
+ upcase(slice(str, 0..0)) + downcase(slice(str, 1..-1) || '')
193
+ end
194
+
195
+ # ///
196
+ # /// Extra String methods for unicode operations
197
+ # ///
198
+
199
+ # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
200
+ # passing strings to databases and validations.
201
+ #
202
+ # * <tt>str</tt>: The string to perform normalization on.
203
+ # * <tt>form</tt>: The form you want to normalize in. Should be one of the following: :c, :kc, :d or :kd.
204
+ def normalize(str, form=ActiveSupport::Multibyte::DEFAULT_NORMALIZATION_FORM)
205
+ # See http://www.unicode.org/reports/tr15, Table 1
206
+ codepoints = u_unpack(str)
207
+ case form
208
+ when :d
209
+ reorder_characters(decompose_codepoints(:canonical, codepoints))
210
+ when :c
211
+ compose_codepoints reorder_characters(decompose_codepoints(:canonical, codepoints))
212
+ when :kd
213
+ reorder_characters(decompose_codepoints(:compatability, codepoints))
214
+ when :kc
215
+ compose_codepoints reorder_characters(decompose_codepoints(:compatability, codepoints))
216
+ else
217
+ raise ArgumentError, "#{form} is not a valid normalization variant", caller
218
+ end.pack('U*')
219
+ end
220
+
221
+ # Perform decomposition on the characters in the string
222
+ def decompose(str)
223
+ decompose_codepoints(:canonical, u_unpack(str)).pack('U*')
224
+ end
225
+
226
+ # Perform composition on the characters in the string
227
+ def compose(str)
228
+ compose_codepoints u_unpack(str).pack('U*')
229
+ end
230
+
231
+ # ///
232
+ # /// BEGIN Helper methods for unicode operation
233
+ # ///
234
+
235
+ # Used to translate an offset from bytes to characters, for instance one received from a regular expression match
236
+ def translate_offset(str, byte_offset)
237
+ return 0 if str == ''
238
+ return nil if byte_offset.nil?
239
+ chunk = str[0..byte_offset]
240
+ begin
241
+ begin
242
+ chunk.unpack('U*').length - 1
243
+ rescue ArgumentError => e
244
+ chunk = str[0..(byte_offset+=1)]
245
+ # Stop retrying at the end of the string
246
+ raise e unless byte_offset < chunk.length
247
+ # We damaged a character, retry
248
+ retry
249
+ end
250
+ # Catch the ArgumentError so we can throw our own
251
+ rescue ArgumentError
252
+ raise EncodingError.new('malformed UTF-8 character')
253
+ end
254
+ end
255
+
256
+ # Checks if the string is valid UTF8.
257
+ def consumes?(str)
258
+ # Unpack is a little bit faster than regular expressions
259
+ begin
260
+ str.unpack('U*')
261
+ true
262
+ rescue ArgumentError
263
+ false
264
+ end
265
+ end
266
+
267
+ # Returns the number of grapheme clusters in the string. This method is very likely to be moved or renamed
268
+ # in future versions.
269
+ def g_length(str)
270
+ g_unpack(str).length
271
+ end
272
+
273
+ # Replaces all the non-utf-8 bytes by their iso-8859-1 or cp1252 equivalent resulting in a valid utf-8 string
274
+ def tidy_bytes(str)
275
+ str.split(//u).map do |c|
276
+ if !UTF8_PAT.match(c)
277
+ n = c.unpack('C')[0]
278
+ n < 128 ? n.chr :
279
+ n < 160 ? [UCD.cp1252[n] || n].pack('U') :
280
+ n < 192 ? "\xC2" + n.chr : "\xC3" + (n-64).chr
281
+ else
282
+ c
283
+ end
284
+ end.join
285
+ end
286
+
287
+ protected
288
+
289
+ # Detect whether the codepoint is in a certain character class. Primarily used by the
290
+ # grapheme cluster support.
291
+ def in_char_class?(codepoint, classes)
292
+ classes.detect { |c| UCD.boundary[c] === codepoint } ? true : false
293
+ end
294
+
295
+ # Unpack the string at codepoints boundaries
296
+ def u_unpack(str)
297
+ begin
298
+ str.unpack 'U*'
299
+ rescue ArgumentError
300
+ raise EncodingError.new('malformed UTF-8 character')
301
+ end
302
+ end
303
+
304
+ # Unpack the string at grapheme boundaries instead of codepoint boundaries
305
+ def g_unpack(str)
306
+ codepoints = u_unpack(str)
307
+ unpacked = []
308
+ pos = 0
309
+ marker = 0
310
+ eoc = codepoints.length
311
+ while(pos < eoc)
312
+ pos += 1
313
+ previous = codepoints[pos-1]
314
+ current = codepoints[pos]
315
+ if (
316
+ # CR X LF
317
+ one = ( previous == UCD.boundary[:cr] and current == UCD.boundary[:lf] ) or
318
+ # L X (L|V|LV|LVT)
319
+ two = ( UCD.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or
320
+ # (LV|V) X (V|T)
321
+ three = ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or
322
+ # (LVT|T) X (T)
323
+ four = ( in_char_class?(previous, [:lvt,:t]) and UCD.boundary[:t] === current ) or
324
+ # X Extend
325
+ five = (UCD.boundary[:extend] === current)
326
+ )
327
+ else
328
+ unpacked << codepoints[marker..pos-1]
329
+ marker = pos
330
+ end
331
+ end
332
+ unpacked
333
+ end
334
+
335
+ # Reverse operation of g_unpack
336
+ def g_pack(unpacked)
337
+ unpacked.flatten
338
+ end
339
+
340
+ # Convert characters to a different case
341
+ def to_case(way, str)
342
+ u_unpack(str).map do |codepoint|
343
+ cp = UCD[codepoint]
344
+ unless cp.nil?
345
+ ncp = cp.send(way)
346
+ ncp > 0 ? ncp : codepoint
347
+ else
348
+ codepoint
349
+ end
350
+ end.pack('U*')
351
+ end
352
+
353
+ # Re-order codepoints so the string becomes canonical
354
+ def reorder_characters(codepoints)
355
+ length = codepoints.length- 1
356
+ pos = 0
357
+ while pos < length do
358
+ cp1, cp2 = UCD[codepoints[pos]], UCD[codepoints[pos+1]]
359
+ if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
360
+ codepoints[pos..pos+1] = cp2.code, cp1.code
361
+ pos += (pos > 0 ? -1 : 1)
362
+ else
363
+ pos += 1
364
+ end
365
+ end
366
+ codepoints
367
+ end
368
+
369
+ # Decompose composed characters to the decomposed form
370
+ def decompose_codepoints(type, codepoints)
371
+ codepoints.inject([]) do |decomposed, cp|
372
+ # if it's a hangul syllable starter character
373
+ if HANGUL_SBASE <= cp and cp < HANGUL_SLAST
374
+ sindex = cp - HANGUL_SBASE
375
+ ncp = [] # new codepoints
376
+ ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
377
+ ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
378
+ tindex = sindex % HANGUL_TCOUNT
379
+ ncp << (HANGUL_TBASE + tindex) unless tindex == 0
380
+ decomposed.concat ncp
381
+ # if the codepoint is decomposable in with the current decomposition type
382
+ elsif (ncp = UCD[cp].decomp_mapping) and (!UCD[cp].decomp_type || type == :compatability)
383
+ decomposed.concat decompose_codepoints(type, ncp.dup)
384
+ else
385
+ decomposed << cp
386
+ end
387
+ end
388
+ end
389
+
390
+ # Compose decomposed characters to the composed form
391
+ def compose_codepoints(codepoints)
392
+ pos = 0
393
+ eoa = codepoints.length - 1
394
+ starter_pos = 0
395
+ starter_char = codepoints[0]
396
+ previous_combining_class = -1
397
+ while pos < eoa
398
+ pos += 1
399
+ lindex = starter_char - HANGUL_LBASE
400
+ # -- Hangul
401
+ if 0 <= lindex and lindex < HANGUL_LCOUNT
402
+ vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1
403
+ if 0 <= vindex and vindex < HANGUL_VCOUNT
404
+ tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1
405
+ if 0 <= tindex and tindex < HANGUL_TCOUNT
406
+ j = starter_pos + 2
407
+ eoa -= 2
408
+ else
409
+ tindex = 0
410
+ j = starter_pos + 1
411
+ eoa -= 1
412
+ end
413
+ codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
414
+ end
415
+ starter_pos += 1
416
+ starter_char = codepoints[starter_pos]
417
+ # -- Other characters
418
+ else
419
+ current_char = codepoints[pos]
420
+ current = UCD[current_char]
421
+ if current.combining_class > previous_combining_class
422
+ if ref = UCD.composition_map[starter_char]
423
+ composition = ref[current_char]
424
+ else
425
+ composition = nil
426
+ end
427
+ unless composition.nil?
428
+ codepoints[starter_pos] = composition
429
+ starter_char = composition
430
+ codepoints.delete_at pos
431
+ eoa -= 1
432
+ pos -= 1
433
+ previous_combining_class = -1
434
+ else
435
+ previous_combining_class = current.combining_class
436
+ end
437
+ else
438
+ previous_combining_class = current.combining_class
439
+ end
440
+ if current.combining_class == 0
441
+ starter_pos = pos
442
+ starter_char = codepoints[pos]
443
+ end
444
+ end
445
+ end
446
+ codepoints
447
+ end
448
+
449
+ # UniCode Database
450
+ UCD = UnicodeDatabase.new
451
+ end
452
+ end
453
+ end