Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed StringWidth() implementation by using proper Unicode grapheme cluster segmentation. Fixes #28 #29

Merged
merged 7 commits into from Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
@@ -1,3 +1,5 @@
module github.com/mattn/go-runewidth

go 1.9

require github.com/rivo/uniseg v0.1.0
2 changes: 2 additions & 0 deletions 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=
80 changes: 32 additions & 48 deletions runewidth.go
Expand Up @@ -2,6 +2,8 @@ package runewidth

import (
"os"

"github.com/rivo/uniseg"
)

//go:generate go run script/generate.go
Expand All @@ -10,9 +12,6 @@ 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{}
)
Expand All @@ -30,7 +29,6 @@ func handleEnv() {
}
// update DefaultCondition
DefaultCondition.EastAsianWidth = EastAsianWidth
DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
}

type interval struct {
Expand Down Expand Up @@ -85,15 +83,13 @@ var nonprint = 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,
}
}

Expand All @@ -110,66 +106,54 @@ 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) {
if width < w {
width = w
}
} else {
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 = 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.
}
}
width += chWidth
}
return c.stringWidth(s)
return
}

// Truncate return string truncated with w cells
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 = c.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
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
Expand Down
7 changes: 3 additions & 4 deletions runewidth_test.go
Expand Up @@ -243,7 +243,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)
}
}
}
Expand Down Expand Up @@ -400,9 +400,8 @@ func TestEnv(t *testing.T) {
}
}

func TestZeroWidthJointer(t *testing.T) {
func TestZeroWidthJoiner(t *testing.T) {
c := NewCondition()
c.ZeroWidthJoiner = true

var tests = []struct {
in string
Expand All @@ -414,7 +413,7 @@ func TestZeroWidthJointer(t *testing.T) {
{"‍🍳", 2},
{"👨‍👨", 2},
{"👨‍👨‍👧", 2},
{"🏳️‍🌈", 2},
{"🏳️‍🌈", 1},
{"あ👩‍🍳い", 6},
{"あ‍🍳い", 6},
{"あ‍い", 4},
Expand Down