Skip to content

Commit

Permalink
Pulling Active Support multibyte requirements over for Ruby 1.8.7
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikel Lindsaar committed Apr 26, 2011
1 parent 3c4c2c8 commit 6eb4c44
Show file tree
Hide file tree
Showing 17 changed files with 1,336 additions and 81 deletions.
1 change: 0 additions & 1 deletion Gemfile
@@ -1,6 +1,5 @@
source :rubygems

gem "activesupport", ">= 2.3.6"
gem "tlsmail" if RUBY_VERSION <= '1.8.6'
gem "mime-types", "~> 1.16"
gem "treetop", "~> 1.4.8"
Expand Down
16 changes: 11 additions & 5 deletions lib/mail.rb
Expand Up @@ -26,13 +26,19 @@ module Mail # :doc:

require 'mail/version'

require 'mail/core_extensions/nil'
require 'mail/core_extensions/string'
# Only load our extensions if AS is not already loaded
unless defined?(ActiveSupport)
require 'mail/core_extensions/nil'
require 'mail/core_extensions/string'
require 'mail/core_extensions/string/access'
require 'mail/core_extensions/string/multibyte'
require 'mail/core_extensions/object'
require 'mail/multibyte'
require 'mail/indifferent_hash'
end

require 'mail/core_extensions/shellwords' unless String.new.respond_to?(:shellescape)
require 'mail/core_extensions/smtp' if RUBY_VERSION < '1.9.3'
require 'mail/core_extensions/object'

require 'mail/indifferent_hash'

require 'mail/patterns'
require 'mail/utilities'
Expand Down
98 changes: 98 additions & 0 deletions lib/mail/core_extensions/string/access.rb
@@ -0,0 +1,98 @@

class String
unless '1.9'.respond_to?(:force_encoding)
# Returns the character at the +position+ treating the string as an array (where 0 is the first character).
#
# Examples:
# "hello".at(0) # => "h"
# "hello".at(4) # => "o"
# "hello".at(10) # => ERROR if < 1.9, nil in 1.9
def at(position)
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).
#
# Examples:
# "hello".from(0) # => "hello"
# "hello".from(2) # => "llo"
# "hello".from(10) # => "" if < 1.9, nil in 1.9
def from(position)
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).
#
# Examples:
# "hello".to(0) # => "h"
# "hello".to(2) # => "hel"
# "hello".to(10) # => "hello"
def to(position)
mb_chars[0..position].to_s
end

# Returns the first character of the string or the first +limit+ characters.
#
# Examples:
# "hello".first # => "h"
# "hello".first(2) # => "he"
# "hello".first(10) # => "hello"
def first(limit = 1)
if limit == 0
''
elsif limit >= size
self
else
mb_chars[0...limit].to_s
end
end

# Returns the last character of the string or the last +limit+ characters.
#
# Examples:
# "hello".last # => "o"
# "hello".last(2) # => "lo"
# "hello".last(10) # => "hello"
def last(limit = 1)
if limit == 0
''
elsif limit >= size
self
else
mb_chars[(-limit)..-1].to_s
end
end
else
def at(position)
self[position]
end

def from(position)
self[position..-1]
end

def to(position)
self[0..position]
end

def first(limit = 1)
if limit == 0
''
elsif limit >= size
self
else
to(limit - 1)
end
end

def last(limit = 1)
if limit == 0
''
elsif limit >= size
self
else
from(-limit)
end
end
end
end
72 changes: 72 additions & 0 deletions lib/mail/core_extensions/string/multibyte.rb
@@ -0,0 +1,72 @@
# encoding: utf-8
require 'mail/multibyte'

class String
if RUBY_VERSION >= "1.9"
# == 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 Mail::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. This means that
# it becomes easy to run one version of your code 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 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 Mail::Multibyte::Chars. For
# information about how to change the default Multibyte behaviour see Mail::Multibyte.
def mb_chars
if Mail::Multibyte.proxy_class.consumes?(self)
Mail::Multibyte.proxy_class.new(self)
else
self
end
end

def is_utf8? #:nodoc
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
else
def mb_chars
if Mail::Multibyte.proxy_class.wants?(self)
Mail::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?
Mail::Multibyte::Chars.consumes?(self)
end
end
end
136 changes: 126 additions & 10 deletions lib/mail/indifferent_hash.rb
@@ -1,26 +1,142 @@
module Mail
# Sort of like ActiveSupport HashWithIndifferentAccess, but lighter
class IndifferentHash < Hash
def initialize(other=nil)
if other.is_a?(Hash)
self.default = other.default
self.update(other)

def initialize(constructor = {})
if constructor.is_a?(Hash)
super()
update(constructor)
else
super(constructor)
end
end

def default(key = nil)
if key.is_a?(Symbol) && include?(key = key.to_s)
self[key]
else
super
end
end

def [](key_name)
super(key_name.to_sym)
def self.new_from_hash_copying_default(hash)
IndifferentHash.new(hash).tap do |new_hash|
new_hash.default = hash.default
end
end

def []=(k, v)
super(k.to_sym, v)
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
alias_method :regular_update, :update unless method_defined?(:regular_update)

# Assigns a new value to the hash:
#
# hash = HashWithIndifferentAccess.new
# hash[:key] = "value"
#
def []=(key, value)
regular_writer(convert_key(key), convert_value(value))
end

alias_method :store, :[]=

# Updates the instantized hash with values from the second:
#
# hash_1 = HashWithIndifferentAccess.new
# hash_1[:key] = "value"
#
# hash_2 = HashWithIndifferentAccess.new
# hash_2[:key] = "New Value!"
#
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
#
def update(other_hash)
super(other_hash.inject({}) {|c, (k, v)| c[k.to_sym] = v; c})
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
self
end
alias merge! update

alias_method :merge!, :update

# Checks the hash for a key matching the argument passed in:
#
# hash = HashWithIndifferentAccess.new
# hash["key"] = "value"
# hash.key? :key # => true
# hash.key? "key" # => true
#
def key?(key)
super(convert_key(key))
end

alias_method :include?, :key?
alias_method :has_key?, :key?
alias_method :member?, :key?

# Fetches the value for the specified key, same as doing hash[key]
def fetch(key, *extras)
super(convert_key(key), *extras)
end

# Returns an array of the values at the specified indices:
#
# hash = HashWithIndifferentAccess.new
# hash[:a] = "x"
# hash[:b] = "y"
# hash.values_at("a", "b") # => ["x", "y"]
#
def values_at(*indices)
indices.collect {|key| self[convert_key(key)]}
end

# Returns an exact copy of the hash.
def dup
IndifferentHash.new(self)
end

# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
# Does not overwrite the existing hash.
def merge(hash)
self.dup.update(hash)
end

# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
def reverse_merge(other_hash)
super self.class.new_from_hash_copying_default(other_hash)
end

def reverse_merge!(other_hash)
replace(reverse_merge( other_hash ))
end

# Removes a specified key from the hash.
def delete(key)
super(convert_key(key))
end

def stringify_keys!; self end
def stringify_keys; dup end
def symbolize_keys; to_hash.symbolize_keys end
def to_options!; self end

def to_hash
Hash.new(default).merge!(self)
end

protected

def convert_key(key)
key.kind_of?(Symbol) ? key.to_s : key
end

def convert_value(value)
if value.class == Hash
self.class.new_from_hash_copying_default(value)
elsif value.is_a?(Array)
value.dup.replace(value.map { |e| convert_value(e) })
else
value
end
end

end
end

0 comments on commit 6eb4c44

Please sign in to comment.