|
1 | 1 | package text |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "golang.org/x/text/width" |
| 4 | + runewidth "github.com/mattn/go-runewidth" |
| 5 | + "github.com/rivo/uniseg" |
| 6 | +) |
| 7 | + |
| 8 | +const ( |
| 9 | + ellipsisWidth = 3 |
| 10 | + minWidthForEllipsis = 5 |
5 | 11 | ) |
6 | 12 |
|
7 | 13 | // DisplayWidth calculates what the rendered width of a string may be |
8 | 14 | func DisplayWidth(s string) int { |
| 15 | + g := uniseg.NewGraphemes(s) |
9 | 16 | w := 0 |
10 | | - for _, r := range s { |
11 | | - w += runeDisplayWidth(r) |
| 17 | + for g.Next() { |
| 18 | + w += graphemeWidth(g) |
12 | 19 | } |
13 | 20 | return w |
14 | 21 | } |
15 | 22 |
|
16 | | -const ( |
17 | | - ellipsisWidth = 3 |
18 | | - minWidthForEllipsis = 5 |
19 | | -) |
20 | | - |
21 | 23 | // Truncate shortens a string to fit the maximum display width |
22 | | -func Truncate(max int, s string) string { |
| 24 | +func Truncate(maxWidth int, s string) string { |
23 | 25 | w := DisplayWidth(s) |
24 | | - if w <= max { |
| 26 | + if w <= maxWidth { |
25 | 27 | return s |
26 | 28 | } |
27 | 29 |
|
28 | 30 | useEllipsis := false |
29 | | - if max >= minWidthForEllipsis { |
| 31 | + if maxWidth >= minWidthForEllipsis { |
30 | 32 | useEllipsis = true |
31 | | - max -= ellipsisWidth |
| 33 | + maxWidth -= ellipsisWidth |
32 | 34 | } |
33 | 35 |
|
34 | | - cw := 0 |
35 | | - ri := 0 |
36 | | - for _, r := range s { |
37 | | - rw := runeDisplayWidth(r) |
38 | | - if cw+rw > max { |
| 36 | + g := uniseg.NewGraphemes(s) |
| 37 | + r := "" |
| 38 | + rWidth := 0 |
| 39 | + for { |
| 40 | + g.Next() |
| 41 | + gWidth := graphemeWidth(g) |
| 42 | + |
| 43 | + if rWidth+gWidth <= maxWidth { |
| 44 | + r += g.Str() |
| 45 | + rWidth += gWidth |
| 46 | + continue |
| 47 | + } else { |
39 | 48 | break |
40 | 49 | } |
41 | | - cw += rw |
42 | | - ri++ |
43 | 50 | } |
44 | 51 |
|
45 | | - res := string([]rune(s)[:ri]) |
46 | 52 | if useEllipsis { |
47 | | - res += "..." |
| 53 | + r += "..." |
48 | 54 | } |
49 | | - if cw < max { |
50 | | - // compensate if truncating a wide character left an odd space |
51 | | - res += " " |
| 55 | + |
| 56 | + if rWidth < maxWidth { |
| 57 | + r += " " |
52 | 58 | } |
53 | | - return res |
54 | | -} |
55 | 59 |
|
56 | | -var runeDisplayWidthOverrides = map[rune]int{ |
57 | | - '“': 1, |
58 | | - '”': 1, |
59 | | - '‘': 1, |
60 | | - '’': 1, |
61 | | - '–': 1, // en dash |
62 | | - '—': 1, // em dash |
63 | | - '→': 1, |
64 | | - '…': 1, |
65 | | - '•': 1, // bullet |
66 | | - '·': 1, // middle dot |
| 60 | + return r |
67 | 61 | } |
68 | 62 |
|
69 | | -func runeDisplayWidth(r rune) int { |
70 | | - if w, ok := runeDisplayWidthOverrides[r]; ok { |
71 | | - return w |
72 | | - } |
73 | | - |
74 | | - switch width.LookupRune(r).Kind() { |
75 | | - case width.EastAsianWide, width.EastAsianAmbiguous, width.EastAsianFullwidth: |
76 | | - return 2 |
77 | | - default: |
78 | | - return 1 |
| 63 | +// graphemeWidth calculates what the rendered width of a grapheme may be |
| 64 | +func graphemeWidth(g *uniseg.Graphemes) int { |
| 65 | + // If grapheme spans one rune use that rune width |
| 66 | + // If grapheme spans multiple runes use the first non-zero rune width |
| 67 | + w := 0 |
| 68 | + for _, r := range g.Runes() { |
| 69 | + w = runewidth.RuneWidth(r) |
| 70 | + if w > 0 { |
| 71 | + break |
| 72 | + } |
79 | 73 | } |
| 74 | + return w |
80 | 75 | } |
0 commit comments