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.
- data/CHANGELOG +232 -2
- data/README +43 -0
- data/lib/active_support.rb +4 -1
- data/lib/active_support/breakpoint.rb +5 -0
- data/lib/active_support/core_ext/array.rb +2 -16
- data/lib/active_support/core_ext/array/conversions.rb +30 -4
- data/lib/active_support/core_ext/array/grouping.rb +55 -0
- data/lib/active_support/core_ext/bigdecimal.rb +3 -0
- data/lib/active_support/core_ext/bigdecimal/formatting.rb +7 -0
- data/lib/active_support/core_ext/class/inheritable_attributes.rb +6 -1
- data/lib/active_support/core_ext/date/conversions.rb +13 -7
- data/lib/active_support/core_ext/enumerable.rb +41 -10
- data/lib/active_support/core_ext/exception.rb +2 -2
- data/lib/active_support/core_ext/hash/conversions.rb +123 -12
- data/lib/active_support/core_ext/hash/indifferent_access.rb +18 -9
- data/lib/active_support/core_ext/integer/inflections.rb +10 -4
- data/lib/active_support/core_ext/load_error.rb +3 -3
- data/lib/active_support/core_ext/module.rb +2 -0
- data/lib/active_support/core_ext/module/aliasing.rb +58 -0
- data/lib/active_support/core_ext/module/attr_internal.rb +31 -0
- data/lib/active_support/core_ext/module/delegation.rb +27 -2
- data/lib/active_support/core_ext/name_error.rb +20 -0
- data/lib/active_support/core_ext/string.rb +2 -0
- data/lib/active_support/core_ext/string/access.rb +5 -5
- data/lib/active_support/core_ext/string/inflections.rb +93 -4
- data/lib/active_support/core_ext/string/unicode.rb +42 -0
- data/lib/active_support/core_ext/symbol.rb +1 -1
- data/lib/active_support/core_ext/time/calculations.rb +7 -5
- data/lib/active_support/core_ext/time/conversions.rb +1 -2
- data/lib/active_support/dependencies.rb +417 -50
- data/lib/active_support/deprecation.rb +201 -0
- data/lib/active_support/inflections.rb +1 -2
- data/lib/active_support/inflector.rb +117 -19
- data/lib/active_support/json.rb +14 -3
- data/lib/active_support/json/encoders/core.rb +21 -18
- data/lib/active_support/multibyte.rb +7 -0
- data/lib/active_support/multibyte/chars.rb +129 -0
- data/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
- data/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
- data/lib/active_support/multibyte/handlers/utf8_handler.rb +453 -0
- data/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +44 -0
- data/lib/active_support/option_merger.rb +3 -3
- data/lib/active_support/ordered_options.rb +24 -23
- data/lib/active_support/reloadable.rb +39 -5
- data/lib/active_support/values/time_zone.rb +1 -1
- data/lib/active_support/values/unicode_tables.dat +0 -0
- data/lib/active_support/vendor/builder/blankslate.rb +16 -6
- data/lib/active_support/vendor/builder/xchar.rb +112 -0
- data/lib/active_support/vendor/builder/xmlbase.rb +12 -10
- data/lib/active_support/vendor/builder/xmlmarkup.rb +26 -7
- data/lib/active_support/vendor/xml_simple.rb +1021 -0
- data/lib/active_support/version.rb +2 -2
- data/lib/active_support/whiny_nil.rb +1 -1
- metadata +26 -4
- data/lib/active_support/core_ext/hash/conversions.rb.rej +0 -28
data/lib/active_support/json.rb
CHANGED
@@ -4,14 +4,20 @@ module ActiveSupport
|
|
4
4
|
module JSON #:nodoc:
|
5
5
|
class CircularReferenceError < StandardError #:nodoc:
|
6
6
|
end
|
7
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 |
|
54
|
-
|
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,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
|