Skip to content

Commit 99dc2a3

Browse files
committed
Update grapheme cluster width calculation
Width of NonspacingMark and EnclosingMark is 0 Width of char just after ZeroWidthJoiner is 0 Width of Hangul GraphemeClusterBreak=V,T are 0 because there should be preceding L or LV
1 parent 13d11df commit 99dc2a3

4 files changed

Lines changed: 62 additions & 35 deletions

File tree

bin/generate_east_asian_width

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ if ARGV.empty?
55
exit 1
66
end
77

8-
def unicode_width(type, category)
9-
return 0 if category == 'Mn' # Nonspacing Mark
8+
def unicode_width(type, category, rest)
9+
# Nonspacing Mark, Enclosing Mark
10+
return 0 if category == 'Mn' || category == 'Me'
11+
12+
# Grapheme_Cluster_Break=V, Grapheme_Cluster_Break=T.
13+
# Width of L, LV, LVT are 2. Treat V and T as width=0 because there should be L or LV before V or T.
14+
return 0 if rest =~ /HANGUL JUNGSEONG|HANGUL JONGSEONG/
15+
1016
case type
1117
when 'F', 'W' # Fullwidth, Wide
1218
2
@@ -27,10 +33,10 @@ open(ARGV.first, 'rt') do |f|
2733

2834
widths = []
2935
f.each_line do |line|
30-
next unless /^(?<first>\h+)(?:\.\.(?<last>\h+))?\s*;\s*(?<type>\w+)\s+# +(?<category>[^ ]+)/ =~ line
36+
next unless /^(?<first>\h+)(?:\.\.(?<last>\h+))?\s*;\s*(?<type>\w+)\s+# +(?<category>[^ ]+)(?<rest>.*)/ =~ line
3137

3238
range = first.to_i(16)..(last || first).to_i(16)
33-
widths.fill(unicode_width(type, category), range)
39+
widths.fill(unicode_width(type, category, rest), range)
3440
end
3541

3642
# EscapedPairs

lib/reline/unicode.rb

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ class Reline::Unicode
4040
CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/
4141
OSC_REGEXP = /\e\]\d+(?:;[^;\a\e]+)*(?:\a|\e\\)/
4242
WIDTH_SCANNER = /\G(?:(#{NON_PRINTING_START})|(#{NON_PRINTING_END})|(#{CSI_REGEXP})|(#{OSC_REGEXP})|(\X))/o
43-
HALFWIDTH_DAKUTEN = 0xFF9E
44-
HALFWIDTH_HANDAKUTEN = 0xFF9F
4543

4644
def self.escape_for_print(str)
4745
str.chars.map! { |gr|
@@ -74,30 +72,32 @@ def self.safe_encode(str, encoding)
7472

7573
require 'reline/unicode/east_asian_width'
7674

75+
def self.east_asian_width(ord)
76+
chunk_index = EastAsianWidth::CHUNK_LAST.bsearch_index { |o| ord <= o }
77+
size = EastAsianWidth::CHUNK_WIDTH[chunk_index]
78+
size == -1 ? Reline.ambiguous_width : size
79+
end
80+
7781
def self.get_mbchar_width(mbchar)
7882
ord = mbchar.ord
7983
if ord <= 0x1F # in EscapedPairs
8084
return 2
81-
elsif mbchar.length <= 1 && ord <= 0x7E # printable ASCII chars
82-
# ~~~~~~~~~~~~~~~~~~ guard against the following grapheme combination character (e.g., dakuten/handakuten)
85+
elsif mbchar.length == 1 && ord <= 0x7E # printable ASCII chars
8386
return 1
8487
end
8588

8689
utf8_mbchar = mbchar.encode(Encoding::UTF_8)
87-
ord = utf8_mbchar.ord
88-
89-
chunk_index = EastAsianWidth::CHUNK_LAST.bsearch_index { |o| ord <= o }
90-
size = EastAsianWidth::CHUNK_WIDTH[chunk_index]
91-
if size == -1
92-
Reline.ambiguous_width
93-
elsif halfwidth_dakuten_or_handakuten_character?(utf8_mbchar[-1])
94-
if utf8_mbchar.length >= 2 # Whether this is a dakuten or handakuten combination character
95-
utf8_mbchar.each_char.sum { |char| get_mbchar_width(char) }
90+
zwj = false
91+
utf8_mbchar.chars.sum do |c|
92+
if zwj
93+
zwj = false
94+
0
95+
elsif c.ord == 0x200D # Zero Width Joiner
96+
zwj = true
97+
0
9698
else
97-
1
99+
east_asian_width(c.ord)
98100
end
99-
else
100-
size
101101
end
102102
end
103103

@@ -418,11 +418,4 @@ def self.word_character?(s)
418418
def self.space_character?(s)
419419
s.match?(/\s/) if s
420420
end
421-
422-
def self.halfwidth_dakuten_or_handakuten_character?(s)
423-
return false if s.encoding != Encoding::UTF_8 || !s.valid_encoding?
424-
425-
ord = s.ord
426-
ord == HALFWIDTH_DAKUTEN || ord == HALFWIDTH_HANDAKUTEN
427-
end
428421
end

lib/reline/unicode/east_asian_width.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class Reline::Unicode::EastAsianWidth
129129
[0x450, 1],
130130
[0x451, -1],
131131
[0x482, 1],
132-
[0x487, 0],
132+
[0x489, 0],
133133
[0x590, 1],
134134
[0x5bd, 0],
135135
[0x5be, 1],
@@ -356,6 +356,7 @@ class Reline::Unicode::EastAsianWidth
356356
[0x109d, 0],
357357
[0x10ff, 1],
358358
[0x115f, 2],
359+
[0x11ff, 0],
359360
[0x135c, 1],
360361
[0x135f, 0],
361362
[0x1711, 1],
@@ -411,8 +412,6 @@ class Reline::Unicode::EastAsianWidth
411412
[0x1a7e, 1],
412413
[0x1a7f, 0],
413414
[0x1aaf, 1],
414-
[0x1abd, 0],
415-
[0x1abe, 1],
416415
[0x1ace, 0],
417416
[0x1aff, 1],
418417
[0x1b03, 0],
@@ -491,10 +490,6 @@ class Reline::Unicode::EastAsianWidth
491490
[0x20ab, 1],
492491
[0x20ac, -1],
493492
[0x20cf, 1],
494-
[0x20dc, 0],
495-
[0x20e0, 1],
496-
[0x20e1, 0],
497-
[0x20e4, 1],
498493
[0x20f0, 0],
499494
[0x2102, 1],
500495
[0x2103, -1],
@@ -767,7 +762,7 @@ class Reline::Unicode::EastAsianWidth
767762
[0xa48f, 1],
768763
[0xa4c6, 2],
769764
[0xa66e, 1],
770-
[0xa66f, 0],
765+
[0xa672, 0],
771766
[0xa673, 1],
772767
[0xa67d, 0],
773768
[0xa69d, 1],
@@ -840,6 +835,10 @@ class Reline::Unicode::EastAsianWidth
840835
[0xabed, 0],
841836
[0xabff, 1],
842837
[0xd7a3, 2],
838+
[0xd7af, 1],
839+
[0xd7c6, 0],
840+
[0xd7ca, 1],
841+
[0xd7fb, 0],
843842
[0xdfff, 1],
844843
[0xf8ff, -1],
845844
[0xfaff, 2],

test/reline/test_unicode.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,33 @@ def test_halfwidth_dakuten_handakuten_combinations
295295
assert_equal 3, Reline::Unicode.get_mbchar_width("あ゙")
296296
assert_equal 3, Reline::Unicode.get_mbchar_width("紅゙")
297297
end
298+
299+
def test_grapheme_cluster_width
300+
# GB6, GB7, GB8: Hangul syllable
301+
assert_equal 2, Reline::Unicode.get_mbchar_width('한'.unicode_normalize(:nfd))
302+
assert_equal 6, Reline::Unicode.get_mbchar_width('ᄀ' * 3)
303+
304+
# GB9
305+
# Char + NonspacingMark
306+
assert_equal 1, Reline::Unicode.get_mbchar_width('ç'.unicode_normalize(:nfd))
307+
assert_equal 2, Reline::Unicode.get_mbchar_width('ぱ'.unicode_normalize(:nfd))
308+
assert_equal 1, Reline::Unicode.get_mbchar_width("c\u{301}\u{327}")
309+
# '1' + NonspacingMark + EnclosingMark
310+
assert_equal 1, Reline::Unicode.get_mbchar_width('1️⃣')
311+
# Char + SpacingMark
312+
assert_equal 2, Reline::Unicode.get_mbchar_width('কা')
313+
assert_equal 5, Reline::Unicode.get_mbchar_width('ガ゚゙゙')
314+
# Emoji joined with ZeroWidthJoiner
315+
assert_equal 2, Reline::Unicode.get_mbchar_width('👨‍👩‍👧')
316+
assert_equal 7, Reline::Unicode.get_mbchar_width('👨‍👩‍👧゙゚゚゚゙')
317+
318+
# GB9a: Char + GraphemeClusterBreak=SpacingMark
319+
assert_equal 2, Reline::Unicode.get_mbchar_width('คำ')
320+
321+
# GB9c: Consonant + Linker(NonspacingMark) + Consonant
322+
assert_equal 2, Reline::Unicode.get_mbchar_width('क्त')
323+
324+
# GB12, GB13: RegionalIndicator
325+
assert_equal 2, Reline::Unicode.get_mbchar_width('🇯🇵')
326+
end
298327
end

0 commit comments

Comments
 (0)