PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/activesupport/lib/active_support/multibyte/chars.rb

https://github.com/ghar/rails
Ruby | 476 lines | 273 code | 41 blank | 162 comment | 30 complexity | 228ce9f30e5a9a80a18229edac973837 MD5 | raw file
  1. # encoding: utf-8
  2. require 'active_support/core_ext/string/access'
  3. require 'active_support/core_ext/string/behavior'
  4. module ActiveSupport #:nodoc:
  5. module Multibyte #:nodoc:
  6. # Chars enables you to work transparently with UTF-8 encoding 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 +mb_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 ".mb_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".mb_chars.downcase.to_s
  19. #
  20. # The default Chars implementation assumes that the encoding of the string is UTF-8, if you want to handle different
  21. # encodings you can write your own multibyte string handler and configure it through
  22. # ActiveSupport::Multibyte.proxy_class.
  23. #
  24. # class CharsForUTF32
  25. # def size
  26. # @wrapped_string.size / 4
  27. # end
  28. #
  29. # def self.accepts?(string)
  30. # string.length % 4 == 0
  31. # end
  32. # end
  33. #
  34. # ActiveSupport::Multibyte.proxy_class = CharsForUTF32
  35. class Chars
  36. attr_reader :wrapped_string
  37. alias to_s wrapped_string
  38. alias to_str wrapped_string
  39. if RUBY_VERSION >= "1.9"
  40. # Creates a new Chars instance by wrapping _string_.
  41. def initialize(string)
  42. @wrapped_string = string
  43. @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
  44. end
  45. else
  46. def initialize(string) #:nodoc:
  47. @wrapped_string = string
  48. end
  49. end
  50. # Forward all undefined methods to the wrapped string.
  51. def method_missing(method, *args, &block)
  52. if method.to_s =~ /!$/
  53. @wrapped_string.__send__(method, *args, &block)
  54. self
  55. else
  56. result = @wrapped_string.__send__(method, *args, &block)
  57. result.kind_of?(String) ? chars(result) : result
  58. end
  59. end
  60. # Returns +true+ if _obj_ responds to the given method. Private methods are included in the search
  61. # only if the optional second parameter evaluates to +true+.
  62. def respond_to?(method, include_private=false)
  63. super || @wrapped_string.respond_to?(method, include_private)
  64. end
  65. # Enable more predictable duck-typing on String-like classes. See Object#acts_like?.
  66. def acts_like_string?
  67. true
  68. end
  69. # Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise.
  70. def self.consumes?(string)
  71. # Unpack is a little bit faster than regular expressions.
  72. string.unpack('U*')
  73. true
  74. rescue ArgumentError
  75. false
  76. end
  77. include Comparable
  78. # Returns -1, 0, or 1, depending on whether the Chars object is to be sorted before,
  79. # equal or after the object on the right side of the operation. It accepts any object
  80. # that implements +to_s+:
  81. #
  82. # 'é'.mb_chars <=> 'ü'.mb_chars # => -1
  83. #
  84. # See <tt>String#<=></tt> for more details.
  85. def <=>(other)
  86. @wrapped_string <=> other.to_s
  87. end
  88. if RUBY_VERSION < "1.9"
  89. # Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns
  90. # +false+ otherwise.
  91. def self.wants?(string)
  92. $KCODE == 'UTF8' && consumes?(string)
  93. end
  94. # Returns a new Chars object containing the _other_ object concatenated to the string.
  95. #
  96. # Example:
  97. # ('Café'.mb_chars + ' périferôl').to_s # => "Café périferôl"
  98. def +(other)
  99. chars(@wrapped_string + other)
  100. end
  101. # Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset.
  102. #
  103. # Example:
  104. # 'Café périferôl'.mb_chars =~ /ô/ # => 12
  105. def =~(other)
  106. translate_offset(@wrapped_string =~ other)
  107. end
  108. # Inserts the passed string at specified codepoint offsets.
  109. #
  110. # Example:
  111. # 'Café'.mb_chars.insert(4, ' périferôl').to_s # => "Café périferôl"
  112. def insert(offset, fragment)
  113. unpacked = Unicode.u_unpack(@wrapped_string)
  114. unless offset > unpacked.length
  115. @wrapped_string.replace(
  116. Unicode.u_unpack(@wrapped_string).insert(offset, *Unicode.u_unpack(fragment)).pack('U*')
  117. )
  118. else
  119. raise IndexError, "index #{offset} out of string"
  120. end
  121. self
  122. end
  123. # Returns +true+ if contained string contains _other_. Returns +false+ otherwise.
  124. #
  125. # Example:
  126. # 'Café'.mb_chars.include?('é') # => true
  127. def include?(other)
  128. # We have to redefine this method because Enumerable defines it.
  129. @wrapped_string.include?(other)
  130. end
  131. # Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found.
  132. #
  133. # Example:
  134. # 'Café périferôl'.mb_chars.index('ô') # => 12
  135. # 'Café périferôl'.mb_chars.index(/\w/u) # => 0
  136. def index(needle, offset=0)
  137. wrapped_offset = first(offset).wrapped_string.length
  138. index = @wrapped_string.index(needle, wrapped_offset)
  139. index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
  140. end
  141. # Returns the position _needle_ in the string, counting in
  142. # codepoints, searching backward from _offset_ or the end of the
  143. # string. Returns +nil+ if _needle_ isn't found.
  144. #
  145. # Example:
  146. # 'Café périferôl'.mb_chars.rindex('é') # => 6
  147. # 'Café périferôl'.mb_chars.rindex(/\w/u) # => 13
  148. def rindex(needle, offset=nil)
  149. offset ||= length
  150. wrapped_offset = first(offset).wrapped_string.length
  151. index = @wrapped_string.rindex(needle, wrapped_offset)
  152. index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
  153. end
  154. # Returns the number of codepoints in the string
  155. def size
  156. Unicode.u_unpack(@wrapped_string).size
  157. end
  158. alias_method :length, :size
  159. # Strips entire range of Unicode whitespace from the right of the string.
  160. def rstrip
  161. chars(@wrapped_string.gsub(Unicode::TRAILERS_PAT, ''))
  162. end
  163. # Strips entire range of Unicode whitespace from the left of the string.
  164. def lstrip
  165. chars(@wrapped_string.gsub(Unicode::LEADERS_PAT, ''))
  166. end
  167. # Strips entire range of Unicode whitespace from the right and left of the string.
  168. def strip
  169. rstrip.lstrip
  170. end
  171. # Returns the codepoint of the first character in the string.
  172. #
  173. # Example:
  174. # 'こんにちは'.mb_chars.ord # => 12371
  175. def ord
  176. Unicode.u_unpack(@wrapped_string)[0]
  177. end
  178. # Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes.
  179. #
  180. # Example:
  181. #
  182. # "¾ cup".mb_chars.rjust(8).to_s
  183. # # => " ¾ cup"
  184. #
  185. # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace
  186. # # => "   ¾ cup"
  187. def rjust(integer, padstr=' ')
  188. justify(integer, :right, padstr)
  189. end
  190. # Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes.
  191. #
  192. # Example:
  193. #
  194. # "¾ cup".mb_chars.rjust(8).to_s
  195. # # => "¾ cup "
  196. #
  197. # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace
  198. # # => "¾ cup   "
  199. def ljust(integer, padstr=' ')
  200. justify(integer, :left, padstr)
  201. end
  202. # Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes.
  203. #
  204. # Example:
  205. #
  206. # "¾ cup".mb_chars.center(8).to_s
  207. # # => " ¾ cup "
  208. #
  209. # "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace
  210. # # => " ¾ cup  "
  211. def center(integer, padstr=' ')
  212. justify(integer, :center, padstr)
  213. end
  214. else
  215. def =~(other)
  216. @wrapped_string =~ other
  217. end
  218. end
  219. # Works just like <tt>String#split</tt>, with the exception that the items in the resulting list are Chars
  220. # instances instead of String. This makes chaining methods easier.
  221. #
  222. # Example:
  223. # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
  224. def split(*args)
  225. @wrapped_string.split(*args).map { |i| i.mb_chars }
  226. end
  227. # Like <tt>String#[]=</tt>, except instead of byte offsets you specify character offsets.
  228. #
  229. # Example:
  230. #
  231. # s = "Müller"
  232. # s.mb_chars[2] = "e" # Replace character with offset 2
  233. # s
  234. # # => "Müeler"
  235. #
  236. # s = "Müller"
  237. # s.mb_chars[1, 2] = "ö" # Replace 2 characters at character offset 1
  238. # s
  239. # # => "Möler"
  240. def []=(*args)
  241. replace_by = args.pop
  242. # Indexed replace with regular expressions already works
  243. if args.first.is_a?(Regexp)
  244. @wrapped_string[*args] = replace_by
  245. else
  246. result = Unicode.u_unpack(@wrapped_string)
  247. case args.first
  248. when Fixnum
  249. raise IndexError, "index #{args[0]} out of string" if args[0] >= result.length
  250. min = args[0]
  251. max = args[1].nil? ? min : (min + args[1] - 1)
  252. range = Range.new(min, max)
  253. replace_by = [replace_by].pack('U') if replace_by.is_a?(Fixnum)
  254. when Range
  255. raise RangeError, "#{args[0]} out of range" if args[0].min >= result.length
  256. range = args[0]
  257. else
  258. needle = args[0].to_s
  259. min = index(needle)
  260. max = min + Unicode.u_unpack(needle).length - 1
  261. range = Range.new(min, max)
  262. end
  263. result[range] = Unicode.u_unpack(replace_by)
  264. @wrapped_string.replace(result.pack('U*'))
  265. end
  266. end
  267. # Reverses all characters in the string.
  268. #
  269. # Example:
  270. # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
  271. def reverse
  272. chars(Unicode.g_unpack(@wrapped_string).reverse.flatten.pack('U*'))
  273. end
  274. # Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that
  275. # character.
  276. #
  277. # Example:
  278. # 'こんにちは'.mb_chars.slice(2..3).to_s # => "にち"
  279. def slice(*args)
  280. if args.size > 2
  281. raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native
  282. elsif (args.size == 2 && !(args.first.is_a?(Numeric) || args.first.is_a?(Regexp)))
  283. raise TypeError, "cannot convert #{args.first.class} into Integer" # Do as if we were native
  284. elsif (args.size == 2 && !args[1].is_a?(Numeric))
  285. raise TypeError, "cannot convert #{args[1].class} into Integer" # Do as if we were native
  286. elsif args[0].kind_of? Range
  287. cps = Unicode.u_unpack(@wrapped_string).slice(*args)
  288. result = cps.nil? ? nil : cps.pack('U*')
  289. elsif args[0].kind_of? Regexp
  290. result = @wrapped_string.slice(*args)
  291. elsif args.size == 1 && args[0].kind_of?(Numeric)
  292. character = Unicode.u_unpack(@wrapped_string)[args[0]]
  293. result = character && [character].pack('U')
  294. else
  295. cps = Unicode.u_unpack(@wrapped_string).slice(*args)
  296. result = cps && cps.pack('U*')
  297. end
  298. result && chars(result)
  299. end
  300. alias_method :[], :slice
  301. # Limit the byte size of the string to a number of bytes without breaking characters. Usable
  302. # when the storage for a string is limited for some reason.
  303. #
  304. # Example:
  305. # 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
  306. def limit(limit)
  307. slice(0...translate_offset(limit))
  308. end
  309. # Convert characters in the string to uppercase.
  310. #
  311. # Example:
  312. # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
  313. def upcase
  314. chars(Unicode.apply_mapping @wrapped_string, :uppercase_mapping)
  315. end
  316. # Convert characters in the string to lowercase.
  317. #
  318. # Example:
  319. # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
  320. def downcase
  321. chars(Unicode.apply_mapping @wrapped_string, :lowercase_mapping)
  322. end
  323. # Converts the first character to uppercase and the remainder to lowercase.
  324. #
  325. # Example:
  326. # 'über'.mb_chars.capitalize.to_s # => "Über"
  327. def capitalize
  328. (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
  329. end
  330. # Capitalizes the first letter of every word, when possible.
  331. #
  332. # Example:
  333. # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró"
  334. # "日本語".mb_chars.titleize # => "日本語"
  335. def titleize
  336. chars(downcase.to_s.gsub(/\b('?[\S])/u) { Unicode.apply_mapping $1, :uppercase_mapping })
  337. end
  338. alias_method :titlecase, :titleize
  339. # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
  340. # passing strings to databases and validations.
  341. #
  342. # * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
  343. # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
  344. # ActiveSupport::Multibyte::Unicode.default_normalization_form
  345. def normalize(form = nil)
  346. chars(Unicode.normalize(@wrapped_string, form))
  347. end
  348. # Performs canonical decomposition on all the characters.
  349. #
  350. # Example:
  351. # 'é'.length # => 2
  352. # 'é'.mb_chars.decompose.to_s.length # => 3
  353. def decompose
  354. chars(Unicode.decompose_codepoints(:canonical, Unicode.u_unpack(@wrapped_string)).pack('U*'))
  355. end
  356. # Performs composition on all the characters.
  357. #
  358. # Example:
  359. # 'é'.length # => 3
  360. # 'é'.mb_chars.compose.to_s.length # => 2
  361. def compose
  362. chars(Unicode.compose_codepoints(Unicode.u_unpack(@wrapped_string)).pack('U*'))
  363. end
  364. # Returns the number of grapheme clusters in the string.
  365. #
  366. # Example:
  367. # 'क्षि'.mb_chars.length # => 4
  368. # 'क्षि'.mb_chars.g_length # => 3
  369. def g_length
  370. Unicode.g_unpack(@wrapped_string).length
  371. end
  372. # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string.
  373. #
  374. # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1.
  375. def tidy_bytes(force = false)
  376. chars(Unicode.tidy_bytes(@wrapped_string, force))
  377. end
  378. %w(capitalize downcase lstrip reverse rstrip slice strip tidy_bytes upcase).each do |method|
  379. # Only define a corresponding bang method for methods defined in the proxy; On 1.9 the proxy will
  380. # exclude lstrip!, rstrip! and strip! because they are already work as expected on multibyte strings.
  381. if public_method_defined?(method)
  382. define_method("#{method}!") do |*args|
  383. @wrapped_string = send(args.nil? ? method : method, *args).to_s
  384. self
  385. end
  386. end
  387. end
  388. protected
  389. def translate_offset(byte_offset) #:nodoc:
  390. return nil if byte_offset.nil?
  391. return 0 if @wrapped_string == ''
  392. if @wrapped_string.respond_to?(:force_encoding)
  393. @wrapped_string = @wrapped_string.dup.force_encoding(Encoding::ASCII_8BIT)
  394. end
  395. begin
  396. @wrapped_string[0...byte_offset].unpack('U*').length
  397. rescue ArgumentError
  398. byte_offset -= 1
  399. retry
  400. end
  401. end
  402. def justify(integer, way, padstr=' ') #:nodoc:
  403. raise ArgumentError, "zero width padding" if padstr.length == 0
  404. padsize = integer - size
  405. padsize = padsize > 0 ? padsize : 0
  406. case way
  407. when :right
  408. result = @wrapped_string.dup.insert(0, padding(padsize, padstr))
  409. when :left
  410. result = @wrapped_string.dup.insert(-1, padding(padsize, padstr))
  411. when :center
  412. lpad = padding((padsize / 2.0).floor, padstr)
  413. rpad = padding((padsize / 2.0).ceil, padstr)
  414. result = @wrapped_string.dup.insert(0, lpad).insert(-1, rpad)
  415. end
  416. chars(result)
  417. end
  418. def padding(padsize, padstr=' ') #:nodoc:
  419. if padsize != 0
  420. chars(padstr * ((padsize / Unicode.u_unpack(padstr).size) + 1)).slice(0, padsize)
  421. else
  422. ''
  423. end
  424. end
  425. def chars(string) #:nodoc:
  426. self.class.new(string)
  427. end
  428. end
  429. end
  430. end