@@ -2,6 +2,7 @@ package runewidth
22
33import (
44 "os"
5+ "sort"
56 "strings"
67 "unicode/utf8"
78
@@ -25,13 +26,19 @@ var (
2526)
2627
2728var (
28- zerowidth table // combining + nonprint merged for faster zero-width lookup
29- widewidth table // ambiguous + doublewidth merged for EA path
29+ zerowidth table // combining + nonprint merged for faster zero-width lookup
30+ widewidth table // ambiguous + doublewidth merged for EA path
31+ eastAsianWidth widthTable
32+ eastAsianWidth0 [0x300 ]byte
3033)
3134
3235func init () {
3336 zerowidth = mergeIntervals (combining , nonprint )
3437 widewidth = mergeIntervals (ambiguous , doublewidth )
38+ eastAsianWidth = makeWidthTable (zerowidth , widewidth )
39+ for r := range eastAsianWidth0 {
40+ eastAsianWidth0 [r ] = byte (runeWidthEastAsian (rune (r )))
41+ }
3542 handleEnv ()
3643}
3744
@@ -90,6 +97,14 @@ type interval struct {
9097
9198type table []interval
9299
100+ type widthInterval struct {
101+ first rune
102+ last rune
103+ width byte
104+ }
105+
106+ type widthTable []widthInterval
107+
93108func inTable (r rune , t table ) bool {
94109 if r < t [0 ].first {
95110 return false
@@ -116,6 +131,71 @@ func inTable(r rune, t table) bool {
116131 return false
117132}
118133
134+ func makeWidthTable (zero , two table ) widthTable {
135+ wt := make (widthTable , 0 , len (zero )+ len (two ))
136+ zi := 0
137+ for _ , iv := range two {
138+ start := iv .first
139+ for zi < len (zero ) && zero [zi ].last < start {
140+ zi ++
141+ }
142+ for i := zi ; i < len (zero ) && zero [i ].first <= iv .last ; i ++ {
143+ if start < zero [i ].first {
144+ wt = append (wt , widthInterval {start , zero [i ].first - 1 , 2 })
145+ }
146+ if start <= zero [i ].last {
147+ start = zero [i ].last + 1
148+ }
149+ if start > iv .last {
150+ break
151+ }
152+ }
153+ if start <= iv .last {
154+ wt = append (wt , widthInterval {start , iv .last , 2 })
155+ }
156+ }
157+ for _ , iv := range zero {
158+ wt = append (wt , widthInterval {iv .first , iv .last , 0 })
159+ }
160+ sort .Slice (wt , func (i , j int ) bool {
161+ return wt [i ].first < wt [j ].first
162+ })
163+ return wt
164+ }
165+
166+ func inWidthTable (r rune , t widthTable ) (int , bool ) {
167+ if r < t [0 ].first {
168+ return 0 , false
169+ }
170+ if r > t [len (t )- 1 ].last {
171+ return 0 , false
172+ }
173+
174+ bot := 0
175+ top := len (t ) - 1
176+ for top >= bot {
177+ mid := (bot + top ) >> 1
178+
179+ switch {
180+ case t [mid ].last < r :
181+ bot = mid + 1
182+ case t [mid ].first > r :
183+ top = mid - 1
184+ default :
185+ return int (t [mid ].width ), true
186+ }
187+ }
188+
189+ return 0 , false
190+ }
191+
192+ func runeWidthEastAsian (r rune ) int {
193+ if w , ok := inWidthTable (r , eastAsianWidth ); ok {
194+ return w
195+ }
196+ return 1
197+ }
198+
119199var private = table {
120200 {0x00E000 , 0x00F8FF }, {0x0F0000 , 0x0FFFFD }, {0x100000 , 0x10FFFD },
121201}
@@ -153,34 +233,35 @@ func (c *Condition) RuneWidth(r rune) int {
153233 }
154234 // optimized version, verified by TestRuneWidthChecksums()
155235 if ! c .EastAsianWidth {
156- switch {
157- case r < 0x20 :
236+ if r < 0x20 {
158237 return 0
159- case (r >= 0x7F && r <= 0x9F ) || r == 0xAD : // nonprint
160- return 0
161- case r < 0x300 :
162- return 1
163- case inTable (r , zerowidth ):
238+ }
239+ if (r >= 0x7F && r <= 0x9F ) || r == 0xAD { // nonprint
164240 return 0
165- case inTable (r , doublewidth ):
166- return 2
167- default :
241+ }
242+ if r < 0x300 {
168243 return 1
169244 }
170- } else {
171245 switch {
172246 case inTable (r , zerowidth ):
173247 return 0
174- case inTable (r , narrow ):
175- return 1
176- case inTable (r , widewidth ):
177- return 2
178- case ! c .StrictEmojiNeutral && inTable (r , emoji ):
248+ case inTable (r , doublewidth ):
179249 return 2
180250 default :
181251 return 1
182252 }
183253 }
254+
255+ if r < 0x300 {
256+ return int (eastAsianWidth0 [r ])
257+ }
258+ if w , ok := inWidthTable (r , eastAsianWidth ); ok {
259+ return w
260+ }
261+ if ! c .StrictEmojiNeutral && inTable (r , emoji ) {
262+ return 2
263+ }
264+ return 1
184265}
185266
186267// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
@@ -206,22 +287,33 @@ func (c *Condition) CreateLUT() {
206287
207288// StringWidth return width as you can see
208289func (c * Condition ) StringWidth (s string ) (width int ) {
290+ if len (s ) == 1 {
291+ b := s [0 ]
292+ if b < 0x20 || b == 0x7F {
293+ return 0
294+ }
295+ return 1
296+ }
209297 if len (s ) > 0 && len (s ) <= utf8 .UTFMax {
210298 r , size := utf8 .DecodeRuneInString (s )
211299 if size == len (s ) {
212300 return c .RuneWidth (r )
213301 }
214302 }
215303 // ASCII fast path: no grapheme clustering needed for pure ASCII
216- if isAllASCII (s ) {
217- for i := 0 ; i < len (s ); i ++ {
218- b := s [i ]
219- if b >= 0x20 && b != 0x7F {
220- width ++
221- }
304+ for i := 0 ; i < len (s ); i ++ {
305+ b := s [i ]
306+ if b >= 0x80 {
307+ goto graphemes
308+ }
309+ if b >= 0x20 && b != 0x7F {
310+ width ++
222311 }
223- return
224312 }
313+ return
314+
315+ graphemes:
316+ width = 0
225317 g := graphemes .FromString (s )
226318 for g .Next () {
227319 var chWidth int
@@ -236,15 +328,6 @@ func (c *Condition) StringWidth(s string) (width int) {
236328 return
237329}
238330
239- func isAllASCII (s string ) bool {
240- for i := 0 ; i < len (s ); i ++ {
241- if s [i ] >= 0x80 {
242- return false
243- }
244- }
245- return true
246- }
247-
248331// Truncate return string truncated with w cells
249332func (c * Condition ) Truncate (s string , w int , tail string ) string {
250333 if c .StringWidth (s ) <= w {
0 commit comments