runewidth.go (7757B)
1 package runewidth 2 3 import ( 4 "os" 5 "strings" 6 7 "github.com/clipperhouse/uax29/v2/graphemes" 8 ) 9 10 //go:generate go run script/generate.go 11 12 var ( 13 // EastAsianWidth will be set true if the current locale is CJK 14 EastAsianWidth bool 15 16 // StrictEmojiNeutral should be set false if handle broken fonts 17 StrictEmojiNeutral bool = true 18 19 // DefaultCondition is a condition in current locale 20 DefaultCondition = &Condition{ 21 EastAsianWidth: false, 22 StrictEmojiNeutral: true, 23 } 24 ) 25 26 func init() { 27 handleEnv() 28 } 29 30 func handleEnv() { 31 env := os.Getenv("RUNEWIDTH_EASTASIAN") 32 if env == "" { 33 EastAsianWidth = IsEastAsian() 34 } else { 35 EastAsianWidth = env == "1" 36 } 37 // update DefaultCondition 38 if DefaultCondition.EastAsianWidth != EastAsianWidth { 39 DefaultCondition.EastAsianWidth = EastAsianWidth 40 if len(DefaultCondition.combinedLut) > 0 { 41 DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0] 42 CreateLUT() 43 } 44 } 45 } 46 47 type interval struct { 48 first rune 49 last rune 50 } 51 52 type table []interval 53 54 func inTables(r rune, ts ...table) bool { 55 for _, t := range ts { 56 if inTable(r, t) { 57 return true 58 } 59 } 60 return false 61 } 62 63 func inTable(r rune, t table) bool { 64 if r < t[0].first { 65 return false 66 } 67 if r > t[len(t)-1].last { 68 return false 69 } 70 71 bot := 0 72 top := len(t) - 1 73 for top >= bot { 74 mid := (bot + top) >> 1 75 76 switch { 77 case t[mid].last < r: 78 bot = mid + 1 79 case t[mid].first > r: 80 top = mid - 1 81 default: 82 return true 83 } 84 } 85 86 return false 87 } 88 89 var private = table{ 90 {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD}, 91 } 92 93 var nonprint = table{ 94 {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, 95 {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, 96 {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, 97 {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, 98 } 99 100 // Condition have flag EastAsianWidth whether the current locale is CJK or not. 101 type Condition struct { 102 combinedLut []byte 103 EastAsianWidth bool 104 StrictEmojiNeutral bool 105 } 106 107 // NewCondition return new instance of Condition which is current locale. 108 func NewCondition() *Condition { 109 return &Condition{ 110 EastAsianWidth: EastAsianWidth, 111 StrictEmojiNeutral: StrictEmojiNeutral, 112 } 113 } 114 115 // RuneWidth returns the number of cells in r. 116 // See http://www.unicode.org/reports/tr11/ 117 func (c *Condition) RuneWidth(r rune) int { 118 if r < 0 || r > 0x10FFFF { 119 return 0 120 } 121 if len(c.combinedLut) > 0 { 122 return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3 123 } 124 // optimized version, verified by TestRuneWidthChecksums() 125 if !c.EastAsianWidth { 126 switch { 127 case r < 0x20: 128 return 0 129 case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint 130 return 0 131 case r < 0x300: 132 return 1 133 case inTable(r, narrow): 134 return 1 135 case inTables(r, nonprint, combining): 136 return 0 137 case inTable(r, doublewidth): 138 return 2 139 default: 140 return 1 141 } 142 } else { 143 switch { 144 case inTables(r, nonprint, combining): 145 return 0 146 case inTable(r, narrow): 147 return 1 148 case inTables(r, ambiguous, doublewidth): 149 return 2 150 case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow): 151 return 2 152 default: 153 return 1 154 } 155 } 156 } 157 158 // CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation. 159 // This should not be called concurrently with other operations on c. 160 // If options in c is changed, CreateLUT should be called again. 161 func (c *Condition) CreateLUT() { 162 const max = 0x110000 163 lut := c.combinedLut 164 if len(c.combinedLut) != 0 { 165 // Remove so we don't use it. 166 c.combinedLut = nil 167 } else { 168 lut = make([]byte, max/2) 169 } 170 for i := range lut { 171 i32 := int32(i * 2) 172 x0 := c.RuneWidth(i32) 173 x1 := c.RuneWidth(i32 + 1) 174 lut[i] = uint8(x0) | uint8(x1)<<4 175 } 176 c.combinedLut = lut 177 } 178 179 // StringWidth return width as you can see 180 func (c *Condition) StringWidth(s string) (width int) { 181 g := graphemes.FromString(s) 182 for g.Next() { 183 var chWidth int 184 for _, r := range g.Value() { 185 chWidth = c.RuneWidth(r) 186 if chWidth > 0 { 187 break // Our best guess at this point is to use the width of the first non-zero-width rune. 188 } 189 } 190 width += chWidth 191 } 192 return 193 } 194 195 // Truncate return string truncated with w cells 196 func (c *Condition) Truncate(s string, w int, tail string) string { 197 if c.StringWidth(s) <= w { 198 return s 199 } 200 w -= c.StringWidth(tail) 201 var width int 202 pos := len(s) 203 g := graphemes.FromString(s) 204 for g.Next() { 205 var chWidth int 206 for _, r := range g.Value() { 207 chWidth = c.RuneWidth(r) 208 if chWidth > 0 { 209 break // See StringWidth() for details. 210 } 211 } 212 if width+chWidth > w { 213 pos = g.Start() 214 break 215 } 216 width += chWidth 217 } 218 return s[:pos] + tail 219 } 220 221 // TruncateLeft cuts w cells from the beginning of the `s`. 222 func (c *Condition) TruncateLeft(s string, w int, prefix string) string { 223 if c.StringWidth(s) <= w { 224 return prefix 225 } 226 227 var width int 228 pos := len(s) 229 230 g := graphemes.FromString(s) 231 for g.Next() { 232 var chWidth int 233 for _, r := range g.Value() { 234 chWidth = c.RuneWidth(r) 235 if chWidth > 0 { 236 break // See StringWidth() for details. 237 } 238 } 239 240 if width+chWidth > w { 241 if width < w { 242 pos = g.End() 243 prefix += strings.Repeat(" ", width+chWidth-w) 244 } else { 245 pos = g.Start() 246 } 247 248 break 249 } 250 251 width += chWidth 252 } 253 254 return prefix + s[pos:] 255 } 256 257 // Wrap return string wrapped with w cells 258 func (c *Condition) Wrap(s string, w int) string { 259 width := 0 260 out := "" 261 for _, r := range s { 262 cw := c.RuneWidth(r) 263 if r == '\n' { 264 out += string(r) 265 width = 0 266 continue 267 } else if width+cw > w { 268 out += "\n" 269 width = 0 270 out += string(r) 271 width += cw 272 continue 273 } 274 out += string(r) 275 width += cw 276 } 277 return out 278 } 279 280 // FillLeft return string filled in left by spaces in w cells 281 func (c *Condition) FillLeft(s string, w int) string { 282 width := c.StringWidth(s) 283 count := w - width 284 if count > 0 { 285 b := make([]byte, count) 286 for i := range b { 287 b[i] = ' ' 288 } 289 return string(b) + s 290 } 291 return s 292 } 293 294 // FillRight return string filled in left by spaces in w cells 295 func (c *Condition) FillRight(s string, w int) string { 296 width := c.StringWidth(s) 297 count := w - width 298 if count > 0 { 299 b := make([]byte, count) 300 for i := range b { 301 b[i] = ' ' 302 } 303 return s + string(b) 304 } 305 return s 306 } 307 308 // RuneWidth returns the number of cells in r. 309 // See http://www.unicode.org/reports/tr11/ 310 func RuneWidth(r rune) int { 311 return DefaultCondition.RuneWidth(r) 312 } 313 314 // IsAmbiguousWidth returns whether is ambiguous width or not. 315 func IsAmbiguousWidth(r rune) bool { 316 return inTables(r, private, ambiguous) 317 } 318 319 // IsNeutralWidth returns whether is neutral width or not. 320 func IsNeutralWidth(r rune) bool { 321 return inTable(r, neutral) 322 } 323 324 // StringWidth return width as you can see 325 func StringWidth(s string) (width int) { 326 return DefaultCondition.StringWidth(s) 327 } 328 329 // Truncate return string truncated with w cells 330 func Truncate(s string, w int, tail string) string { 331 return DefaultCondition.Truncate(s, w, tail) 332 } 333 334 // TruncateLeft cuts w cells from the beginning of the `s`. 335 func TruncateLeft(s string, w int, prefix string) string { 336 return DefaultCondition.TruncateLeft(s, w, prefix) 337 } 338 339 // Wrap return string wrapped with w cells 340 func Wrap(s string, w int) string { 341 return DefaultCondition.Wrap(s, w) 342 } 343 344 // FillLeft return string filled in left by spaces in w cells 345 func FillLeft(s string, w int) string { 346 return DefaultCondition.FillLeft(s, w) 347 } 348 349 // FillRight return string filled in left by spaces in w cells 350 func FillRight(s string, w int) string { 351 return DefaultCondition.FillRight(s, w) 352 } 353 354 // CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation. 355 // This should not be called concurrently with other operations. 356 func CreateLUT() { 357 if len(DefaultCondition.combinedLut) > 0 { 358 return 359 } 360 DefaultCondition.CreateLUT() 361 }