blob: f3871a624aca3b93e278bab141270bc1b1c627ea [file] [log] [blame]
Naveen Sampath04696f72022-06-13 15:19:14 +05301package runewidth
2
3import (
4 "os"
5
6 "github.com/rivo/uniseg"
7)
8
9//go:generate go run script/generate.go
10
11var (
12 // EastAsianWidth will be set true if the current locale is CJK
13 EastAsianWidth bool
14
15 // DefaultCondition is a condition in current locale
16 DefaultCondition = &Condition{}
17)
18
19func init() {
20 handleEnv()
21}
22
23func handleEnv() {
24 env := os.Getenv("RUNEWIDTH_EASTASIAN")
25 if env == "" {
26 EastAsianWidth = IsEastAsian()
27 } else {
28 EastAsianWidth = env == "1"
29 }
30 // update DefaultCondition
31 DefaultCondition.EastAsianWidth = EastAsianWidth
32}
33
34type interval struct {
35 first rune
36 last rune
37}
38
39type table []interval
40
41func inTables(r rune, ts ...table) bool {
42 for _, t := range ts {
43 if inTable(r, t) {
44 return true
45 }
46 }
47 return false
48}
49
50func inTable(r rune, t table) bool {
51 if r < t[0].first {
52 return false
53 }
54
55 bot := 0
56 top := len(t) - 1
57 for top >= bot {
58 mid := (bot + top) >> 1
59
60 switch {
61 case t[mid].last < r:
62 bot = mid + 1
63 case t[mid].first > r:
64 top = mid - 1
65 default:
66 return true
67 }
68 }
69
70 return false
71}
72
73var private = table{
74 {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
75}
76
77var nonprint = table{
78 {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
79 {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
80 {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
81 {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
82}
83
84// Condition have flag EastAsianWidth whether the current locale is CJK or not.
85type Condition struct {
86 EastAsianWidth bool
87}
88
89// NewCondition return new instance of Condition which is current locale.
90func NewCondition() *Condition {
91 return &Condition{
92 EastAsianWidth: EastAsianWidth,
93 }
94}
95
96// RuneWidth returns the number of cells in r.
97// See http://www.unicode.org/reports/tr11/
98func (c *Condition) RuneWidth(r rune) int {
99 switch {
100 case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
101 return 0
102 case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
103 return 2
104 default:
105 return 1
106 }
107}
108
109// StringWidth return width as you can see
110func (c *Condition) StringWidth(s string) (width int) {
111 g := uniseg.NewGraphemes(s)
112 for g.Next() {
113 var chWidth int
114 for _, r := range g.Runes() {
115 chWidth = c.RuneWidth(r)
116 if chWidth > 0 {
117 break // Our best guess at this point is to use the width of the first non-zero-width rune.
118 }
119 }
120 width += chWidth
121 }
122 return
123}
124
125// Truncate return string truncated with w cells
126func (c *Condition) Truncate(s string, w int, tail string) string {
127 if c.StringWidth(s) <= w {
128 return s
129 }
130 w -= c.StringWidth(tail)
131 var width int
132 pos := len(s)
133 g := uniseg.NewGraphemes(s)
134 for g.Next() {
135 var chWidth int
136 for _, r := range g.Runes() {
137 chWidth = c.RuneWidth(r)
138 if chWidth > 0 {
139 break // See StringWidth() for details.
140 }
141 }
142 if width+chWidth > w {
143 pos, _ = g.Positions()
144 break
145 }
146 width += chWidth
147 }
148 return s[:pos] + tail
149}
150
151// Wrap return string wrapped with w cells
152func (c *Condition) Wrap(s string, w int) string {
153 width := 0
154 out := ""
155 for _, r := range []rune(s) {
156 cw := c.RuneWidth(r)
157 if r == '\n' {
158 out += string(r)
159 width = 0
160 continue
161 } else if width+cw > w {
162 out += "\n"
163 width = 0
164 out += string(r)
165 width += cw
166 continue
167 }
168 out += string(r)
169 width += cw
170 }
171 return out
172}
173
174// FillLeft return string filled in left by spaces in w cells
175func (c *Condition) FillLeft(s string, w int) string {
176 width := c.StringWidth(s)
177 count := w - width
178 if count > 0 {
179 b := make([]byte, count)
180 for i := range b {
181 b[i] = ' '
182 }
183 return string(b) + s
184 }
185 return s
186}
187
188// FillRight return string filled in left by spaces in w cells
189func (c *Condition) FillRight(s string, w int) string {
190 width := c.StringWidth(s)
191 count := w - width
192 if count > 0 {
193 b := make([]byte, count)
194 for i := range b {
195 b[i] = ' '
196 }
197 return s + string(b)
198 }
199 return s
200}
201
202// RuneWidth returns the number of cells in r.
203// See http://www.unicode.org/reports/tr11/
204func RuneWidth(r rune) int {
205 return DefaultCondition.RuneWidth(r)
206}
207
208// IsAmbiguousWidth returns whether is ambiguous width or not.
209func IsAmbiguousWidth(r rune) bool {
210 return inTables(r, private, ambiguous)
211}
212
213// IsNeutralWidth returns whether is neutral width or not.
214func IsNeutralWidth(r rune) bool {
215 return inTable(r, neutral)
216}
217
218// StringWidth return width as you can see
219func StringWidth(s string) (width int) {
220 return DefaultCondition.StringWidth(s)
221}
222
223// Truncate return string truncated with w cells
224func Truncate(s string, w int, tail string) string {
225 return DefaultCondition.Truncate(s, w, tail)
226}
227
228// Wrap return string wrapped with w cells
229func Wrap(s string, w int) string {
230 return DefaultCondition.Wrap(s, w)
231}
232
233// FillLeft return string filled in left by spaces in w cells
234func FillLeft(s string, w int) string {
235 return DefaultCondition.FillLeft(s, w)
236}
237
238// FillRight return string filled in left by spaces in w cells
239func FillRight(s string, w int) string {
240 return DefaultCondition.FillRight(s, w)
241}