From d3f4cc2d5f902ce25adc66e11a0192b5f259367b Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 29 Aug 2019 17:33:10 +0200 Subject: [PATCH 1/5] Fixed StringWidth() implementation by using proper Unicode grapheme cluster segmentation. Fixes #28 --- go.mod | 2 ++ go.sum | 2 ++ runewidth.go | 49 +++++++++++++++-------------------------------- runewidth_test.go | 3 +-- 4 files changed, 20 insertions(+), 36 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index fa7f4d8..8a9d524 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/mattn/go-runewidth go 1.9 + +require github.com/rivo/uniseg v0.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0213566 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/runewidth.go b/runewidth.go index 3cb9410..254ad08 100644 --- a/runewidth.go +++ b/runewidth.go @@ -2,15 +2,14 @@ package runewidth import ( "os" + + "github.com/rivo/uniseg" ) var ( // EastAsianWidth will be set true if the current locale is CJK EastAsianWidth bool - // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ - ZeroWidthJoiner bool - // DefaultCondition is a condition in current locale DefaultCondition = &Condition{} ) @@ -28,7 +27,6 @@ func handleEnv() { } // update DefaultCondition DefaultCondition.EastAsianWidth = EastAsianWidth - DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner } type interval struct { @@ -806,15 +804,13 @@ var neutral = table{ // Condition have flag EastAsianWidth whether the current locale is CJK or not. type Condition struct { - EastAsianWidth bool - ZeroWidthJoiner bool + EastAsianWidth bool } // NewCondition return new instance of Condition which is current locale. func NewCondition() *Condition { return &Condition{ - EastAsianWidth: EastAsianWidth, - ZeroWidthJoiner: ZeroWidthJoiner, + EastAsianWidth: EastAsianWidth, } } @@ -833,35 +829,20 @@ func (c *Condition) RuneWidth(r rune) int { } } -func (c *Condition) stringWidth(s string) (width int) { - for _, r := range []rune(s) { - width += c.RuneWidth(r) - } - return width -} - -func (c *Condition) stringWidthZeroJoiner(s string) (width int) { - r1, r2 := rune(0), rune(0) - for _, r := range []rune(s) { - if r == 0xFE0E || r == 0xFE0F { - continue - } - w := c.RuneWidth(r) - if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) { - w = 0 - } - width += w - r1, r2 = r2, r - } - return width -} - // StringWidth return width as you can see func (c *Condition) StringWidth(s string) (width int) { - if c.ZeroWidthJoiner { - return c.stringWidthZeroJoiner(s) + g := uniseg.NewGraphemes(s) + for g.Next() { + var chWidth int + for _, r := range g.Runes() { + chWidth = RuneWidth(r) + if chWidth > 0 { + break // Our best guess at this point is to use the width of the first non-zero-width rune. + } + } + width += chWidth } - return c.stringWidth(s) + return } // Truncate return string truncated with w cells diff --git a/runewidth_test.go b/runewidth_test.go index c9e6e9d..3bac357 100644 --- a/runewidth_test.go +++ b/runewidth_test.go @@ -219,7 +219,7 @@ func TestStringWidth(t *testing.T) { c.EastAsianWidth = true for _, tt := range stringwidthtests { if out := c.StringWidth(tt.in); out != tt.eaout { - t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.eaout) + t.Errorf("StringWidth(%q) = %d, want %d (EA)", tt.in, out, tt.eaout) } } } @@ -378,7 +378,6 @@ func TestEnv(t *testing.T) { func TestZeroWidthJointer(t *testing.T) { c := NewCondition() - c.ZeroWidthJoiner = true var tests = []struct { in string From 98b1fa4118a55fc3c1987e1aab21641854bad2f6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 May 2020 19:47:24 +0200 Subject: [PATCH 2/5] Adapted Truncate() function. Fixed table to make failing test case work. --- runewidth.go | 26 ++++++++++++++++---------- runewidth_table.go | 2 +- runewidth_test.go | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/runewidth.go b/runewidth.go index b9c7171..b591d78 100644 --- a/runewidth.go +++ b/runewidth.go @@ -127,19 +127,25 @@ func (c *Condition) Truncate(s string, w int, tail string) string { if c.StringWidth(s) <= w { return s } - r := []rune(s) - tw := c.StringWidth(tail) - w -= tw - width := 0 - i := 0 - for ; i < len(r); i++ { - cw := c.RuneWidth(r[i]) - if width+cw > w { + w -= c.StringWidth(tail) + var width int + pos := len(s) + g := uniseg.NewGraphemes(s) + for g.Next() { + var chWidth int + for _, r := range g.Runes() { + chWidth = RuneWidth(r) + if chWidth > 0 { + break // See StringWidth() for details. + } + } + if width+chWidth > w { + pos, _ = g.Positions() break } - width += cw + width += chWidth } - return string(r[0:i]) + tail + return s[:pos] + tail } // Wrap return string wrapped with w cells diff --git a/runewidth_table.go b/runewidth_table.go index a8ccee5..90d5810 100644 --- a/runewidth_table.go +++ b/runewidth_table.go @@ -47,7 +47,7 @@ var doublewidth = table{ {0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA}, - {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F4, 0x1F3F4}, + {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F3, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4}, diff --git a/runewidth_test.go b/runewidth_test.go index fb96eff..1551ee6 100644 --- a/runewidth_test.go +++ b/runewidth_test.go @@ -41,7 +41,7 @@ var tables = []tableInfo{ {private, "private", 137468, "a4a641206dc8c5de80bd9f03515a54a706a5a4904c7684dc6a33d65c967a51b2"}, {nonprint, "nonprint", 2143, "288904683eb225e7c4c0bd3ee481b53e8dace404ec31d443afdbc4d13729fe95"}, {combining, "combining", 461, "ef1839ee99b2707da7d5592949bd9b40d434fa6462c6da61477bae923389e263"}, - {doublewidth, "doublewidth", 181887, "de2d7a29c94fb2fe471b5fd0c003043845ce59d1823170606b95f9fc8988067a"}, + {doublewidth, "doublewidth", 181888, "c95aa2420e5d89f326d342f43d49457035cf187bcc6cb73c72d4fc66096abf9a"}, {ambiguous, "ambiguous", 138739, "d05e339a10f296de6547ff3d6c5aee32f627f6555477afebd4a3b7e3cf74c9e3"}, {emoji, "emoji", 3791, "bf02b49f5cbee8df150053574d20125164e7f16b5f62aa5971abca3b2f39a8e6"}, {notassigned, "notassigned", 10, "68441e98eca1450efbe857ac051fcc872eed347054dfd0bc662d1c4ee021d69f"}, From d45afa68b11447b7ea5ba1a330a6f350574b26e4 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 15 May 2020 21:29:55 +0900 Subject: [PATCH 3/5] Should use c.RuneWidth --- runewidth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runewidth.go b/runewidth.go index b591d78..f3871a6 100644 --- a/runewidth.go +++ b/runewidth.go @@ -112,7 +112,7 @@ func (c *Condition) StringWidth(s string) (width int) { for g.Next() { var chWidth int for _, r := range g.Runes() { - chWidth = RuneWidth(r) + chWidth = c.RuneWidth(r) if chWidth > 0 { break // Our best guess at this point is to use the width of the first non-zero-width rune. } @@ -134,7 +134,7 @@ func (c *Condition) Truncate(s string, w int, tail string) string { for g.Next() { var chWidth int for _, r := range g.Runes() { - chWidth = RuneWidth(r) + chWidth = c.RuneWidth(r) if chWidth > 0 { break // See StringWidth() for details. } @@ -153,7 +153,7 @@ func (c *Condition) Wrap(s string, w int) string { width := 0 out := "" for _, r := range []rune(s) { - cw := RuneWidth(r) + cw := c.RuneWidth(r) if r == '\n' { out += string(r) width = 0 From 49ce4bdd97efb70cc35817ec90e11a917c5fcaf8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 May 2020 16:25:26 +0200 Subject: [PATCH 4/5] Restored previously reverted changes (which were probably made by mistake). --- runewidth_table.go | 2 +- runewidth_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runewidth_table.go b/runewidth_table.go index b27d77d..f5a8ea7 100644 --- a/runewidth_table.go +++ b/runewidth_table.go @@ -49,7 +49,7 @@ var doublewidth = table{ {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, - {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, + {0x1F3F3, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, diff --git a/runewidth_test.go b/runewidth_test.go index e6c9bac..bee1554 100644 --- a/runewidth_test.go +++ b/runewidth_test.go @@ -41,7 +41,7 @@ var tables = []tableInfo{ {private, "private", 137468, "a4a641206dc8c5de80bd9f03515a54a706a5a4904c7684dc6a33d65c967a51b2"}, {nonprint, "nonprint", 2143, "288904683eb225e7c4c0bd3ee481b53e8dace404ec31d443afdbc4d13729fe95"}, {combining, "combining", 465, "3cce13deb5e23f9f7327f2b1ef162328285a7dcf277a98302a8f7cdd43971268"}, - {doublewidth, "doublewidth", 182440, "3d16eda8650dc2c92d6318d32f0b4a74fda5a278db2d4544b1dd65863394823c"}, + {doublewidth, "doublewidth", 182441, "8700c1a9b8de44364695b081d017b2d7aba3220db2fd72ef150ca3e301a8abaf"}, {ambiguous, "ambiguous", 138739, "d05e339a10f296de6547ff3d6c5aee32f627f6555477afebd4a3b7e3cf74c9e3"}, {emoji, "emoji", 3535, "9ec17351601d49c535658de8d129c1d0ccda2e620669fc39a2faaee7dedcef6d"}, {notassigned, "notassigned", 10, "68441e98eca1450efbe857ac051fcc872eed347054dfd0bc662d1c4ee021d69f"}, From f1f639b53e80fe40abd644a702ed196880b0b345 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 12 Jul 2020 17:30:51 +0200 Subject: [PATCH 5/5] Reverted previous change to table. Also changed rainbow flag test. Today, the rainbow flag only has a width of 1. --- runewidth_table.go | 2 +- runewidth_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runewidth_table.go b/runewidth_table.go index f5a8ea7..b27d77d 100644 --- a/runewidth_table.go +++ b/runewidth_table.go @@ -49,7 +49,7 @@ var doublewidth = table{ {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, - {0x1F3F3, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, + {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, diff --git a/runewidth_test.go b/runewidth_test.go index bee1554..89c943f 100644 --- a/runewidth_test.go +++ b/runewidth_test.go @@ -41,7 +41,7 @@ var tables = []tableInfo{ {private, "private", 137468, "a4a641206dc8c5de80bd9f03515a54a706a5a4904c7684dc6a33d65c967a51b2"}, {nonprint, "nonprint", 2143, "288904683eb225e7c4c0bd3ee481b53e8dace404ec31d443afdbc4d13729fe95"}, {combining, "combining", 465, "3cce13deb5e23f9f7327f2b1ef162328285a7dcf277a98302a8f7cdd43971268"}, - {doublewidth, "doublewidth", 182441, "8700c1a9b8de44364695b081d017b2d7aba3220db2fd72ef150ca3e301a8abaf"}, + {doublewidth, "doublewidth", 182440, "3d16eda8650dc2c92d6318d32f0b4a74fda5a278db2d4544b1dd65863394823c"}, {ambiguous, "ambiguous", 138739, "d05e339a10f296de6547ff3d6c5aee32f627f6555477afebd4a3b7e3cf74c9e3"}, {emoji, "emoji", 3535, "9ec17351601d49c535658de8d129c1d0ccda2e620669fc39a2faaee7dedcef6d"}, {notassigned, "notassigned", 10, "68441e98eca1450efbe857ac051fcc872eed347054dfd0bc662d1c4ee021d69f"}, @@ -413,7 +413,7 @@ func TestZeroWidthJoiner(t *testing.T) { {"‍🍳", 2}, {"👨‍👨", 2}, {"👨‍👨‍👧", 2}, - {"🏳️‍🌈", 2}, + {"🏳️‍🌈", 1}, {"あ👩‍🍳い", 6}, {"あ‍🍳い", 6}, {"あ‍い", 4},