summaryrefslogtreecommitdiff
path: root/lib/unicode_normalize/normalize.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/unicode_normalize/normalize.rb')
-rw-r--r--lib/unicode_normalize/normalize.rb187
1 files changed, 187 insertions, 0 deletions
diff --git a/lib/unicode_normalize/normalize.rb b/lib/unicode_normalize/normalize.rb
new file mode 100644
index 0000000000..0447df8de7
--- /dev/null
+++ b/lib/unicode_normalize/normalize.rb
@@ -0,0 +1,187 @@
+# coding: utf-8
+# frozen_string_literal: false
+
+# Copyright Ayumu Nojima (野島 歩) and Martin J. Dürst (duerst@it.aoyama.ac.jp)
+
+# This file, the companion file tables.rb (autogenerated), and the module,
+# constants, and method defined herein are part of the implementation of the
+# built-in String class, not part of the standard library. They should
+# therefore never be gemified. They implement the methods
+# String#unicode_normalize, String#unicode_normalize!, and String#unicode_normalized?.
+#
+# They are placed here because they are written in Ruby. They are loaded on
+# demand when any of the three methods mentioned above is executed for the
+# first time. This reduces the memory footprint and startup time for scripts
+# and applications that do not use those methods.
+#
+# The name and even the existence of the module UnicodeNormalize and all of its
+# content are purely an implementation detail, and should not be exposed in
+# any test or spec or otherwise.
+
+require_relative 'tables'
+
+# :stopdoc:
+module UnicodeNormalize # :nodoc:
+ ## Constant for max hash capacity to avoid DoS attack
+ MAX_HASH_LENGTH = 18000 # enough for all test cases, otherwise tests get slow
+
+ ## Regular Expressions and Hash Constants
+ REGEXP_D = Regexp.compile(REGEXP_D_STRING, Regexp::EXTENDED)
+ REGEXP_C = Regexp.compile(REGEXP_C_STRING, Regexp::EXTENDED)
+ REGEXP_K = Regexp.compile(REGEXP_K_STRING, Regexp::EXTENDED)
+ NF_HASH_D = Hash.new do |hash, key|
+ hash.shift if hash.length>MAX_HASH_LENGTH # prevent DoS attack
+ hash[key] = nfd_one(key)
+ end
+ NF_HASH_C = Hash.new do |hash, key|
+ hash.shift if hash.length>MAX_HASH_LENGTH # prevent DoS attack
+ hash[key] = nfc_one(key)
+ end
+
+ ## Constants For Hangul
+ # for details such as the meaning of the identifiers below, please see
+ # http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf, pp. 144/145
+ SBASE = 0xAC00
+ LBASE = 0x1100
+ VBASE = 0x1161
+ TBASE = 0x11A7
+ LCOUNT = 19
+ VCOUNT = 21
+ TCOUNT = 28
+ NCOUNT = VCOUNT * TCOUNT
+ SCOUNT = LCOUNT * NCOUNT
+
+ # Unicode-based encodings (except UTF-8)
+ UNICODE_ENCODINGS = [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE,
+ Encoding::GB18030, Encoding::UCS_2BE, Encoding::UCS_4BE]
+
+ ## Hangul Algorithm
+ def self.hangul_decomp_one(target)
+ syllable_index = target.ord - SBASE
+ return target if syllable_index < 0 || syllable_index >= SCOUNT
+ l = LBASE + syllable_index / NCOUNT
+ v = VBASE + (syllable_index % NCOUNT) / TCOUNT
+ t = TBASE + syllable_index % TCOUNT
+ (t==TBASE ? [l, v] : [l, v, t]).pack('U*') + target[1..-1]
+ end
+
+ def self.hangul_comp_one(string)
+ length = string.length
+ if length>1 and 0 <= (lead =string[0].ord-LBASE) and lead < LCOUNT and
+ 0 <= (vowel=string[1].ord-VBASE) and vowel < VCOUNT
+ lead_vowel = SBASE + (lead * VCOUNT + vowel) * TCOUNT
+ if length>2 and 0 < (trail=string[2].ord-TBASE) and trail < TCOUNT
+ (lead_vowel + trail).chr(Encoding::UTF_8) + string[3..-1]
+ else
+ lead_vowel.chr(Encoding::UTF_8) + string[2..-1]
+ end
+ else
+ string
+ end
+ end
+
+ ## Canonical Ordering
+ def self.canonical_ordering_one(string)
+ result = ''
+ unordered = []
+ chars = string.chars
+ n = chars.size
+ chars.each_with_index do |char, i|
+ ccc = CLASS_TABLE[char]
+ if ccc == 0
+ unordered.sort!.each { result << chars[it % n] }
+ unordered.clear
+ result << char
+ else
+ unordered << ccc * n + i
+ end
+ end
+ unordered.sort!.each { result << chars[it % n] }
+ result
+ end
+
+ ## Normalization Forms for Patterns (not whole Strings)
+ def self.nfd_one(string)
+ string = string.chars.map! {|c| DECOMPOSITION_TABLE[c] || c}.join('')
+ canonical_ordering_one(hangul_decomp_one(string))
+ end
+
+ def self.nfc_one(string)
+ nfd_string = nfd_one string
+ start = nfd_string[0]
+ last_class = CLASS_TABLE[start]-1
+ accents = ''
+ result = ''
+ nfd_string[1..-1].each_char do |accent|
+ accent_class = CLASS_TABLE[accent]
+ if last_class<accent_class and composite = COMPOSITION_TABLE[start+accent]
+ start = composite
+ elsif accent_class == 0
+ result << start << accents
+ start = accent
+ accents = ''
+ last_class = -1
+ else
+ accents << accent
+ last_class = accent_class
+ end
+ end
+ hangul_comp_one(result+start+accents)
+ end
+
+ def self.normalize(string, form = :nfc)
+ encoding = string.encoding
+ case encoding
+ when Encoding::UTF_8
+ case form
+ when :nfc then
+ string.gsub REGEXP_C, NF_HASH_C
+ when :nfd then
+ string.gsub REGEXP_D, NF_HASH_D
+ when :nfkc then
+ string.gsub(REGEXP_K, KOMPATIBLE_TABLE).gsub(REGEXP_C, NF_HASH_C)
+ when :nfkd then
+ string.gsub(REGEXP_K, KOMPATIBLE_TABLE).gsub(REGEXP_D, NF_HASH_D)
+ else
+ raise ArgumentError, "Invalid normalization form #{form}."
+ end
+ when Encoding::US_ASCII
+ string
+ when *UNICODE_ENCODINGS
+ normalize(string.encode(Encoding::UTF_8), form).encode(encoding)
+ else
+ raise Encoding::CompatibilityError, "Unicode Normalization not appropriate for #{encoding}"
+ end
+ end
+
+ def self.normalized?(string, form = :nfc)
+ encoding = string.encoding
+ case encoding
+ when Encoding::UTF_8
+ case form
+ when :nfc then
+ string.scan REGEXP_C do |match|
+ return false if NF_HASH_C[match] != match
+ end
+ true
+ when :nfd then
+ string.scan REGEXP_D do |match|
+ return false if NF_HASH_D[match] != match
+ end
+ true
+ when :nfkc then
+ normalized?(string, :nfc) and string !~ REGEXP_K
+ when :nfkd then
+ normalized?(string, :nfd) and string !~ REGEXP_K
+ else
+ raise ArgumentError, "Invalid normalization form #{form}."
+ end
+ when Encoding::US_ASCII
+ true
+ when *UNICODE_ENCODINGS
+ normalized? string.encode(Encoding::UTF_8), form
+ else
+ raise Encoding::CompatibilityError, "Unicode Normalization not appropriate for #{encoding}"
+ end
+ end
+end # module