runewidth.go (16126B)
1 package runewidth 2 3 import ( 4 "os" 5 "sort" 6 "strings" 7 "sync" 8 "sync/atomic" 9 "unicode/utf8" 10 11 "github.com/clipperhouse/uax29/v2/graphemes" 12 ) 13 14 //go:generate go run script/generate.go 15 16 var ( 17 // EastAsianWidth will be set true if the current locale is CJK 18 EastAsianWidth bool 19 20 // StrictEmojiNeutral should be set false if handle broken fonts 21 StrictEmojiNeutral bool = true 22 23 // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ. 24 // 25 // Deprecated: ZWJ sequences are always handled through Unicode 26 // grapheme cluster segmentation now, so this flag has no effect. 27 // It is kept only for compatibility with code written against 28 // v0.0.9 and earlier. 29 ZeroWidthJoiner bool 30 31 // DefaultCondition is a condition in current locale 32 DefaultCondition = &Condition{ 33 EastAsianWidth: false, 34 StrictEmojiNeutral: true, 35 } 36 ) 37 38 var ( 39 zerowidth table // combining + nonprint merged for faster zero-width lookup 40 widewidth table // ambiguous + doublewidth merged for EA path 41 eastAsianWidth widthTable 42 tablesOnce sync.Once 43 44 // strictWidthLUT is mostly built lazily on the first width lookup so 45 // that importing the package costs neither the build time nor the 2 MB 46 // of resident memory; see issue #104. Only the entries below 47 // strictWidthLUTLimit are valid: init fills the first 0x300 entries of 48 // both planes, and the lazy build fills the rest — never rewriting the 49 // low region, so readers of it cannot race with the build. The limit 50 // is loaded with acquire semantics, which makes the non-atomic reads 51 // of the high region safe once it reports 0x110000. Keeping the whole 52 // check down to one compare-and-branch matters: RuneWidth is only a 53 // dozen instructions long. 54 strictWidthLUT [2][0x110000]byte 55 strictWidthLUTLimit atomic.Int32 56 strictWidthLUTOnce sync.Once 57 ) 58 59 func init() { 60 initStrictWidthLUTLow() 61 strictWidthLUTLimit.Store(0x300) 62 handleEnv() 63 } 64 65 // initStrictWidthLUTLow paints the first 0x300 entries of strictWidthLUT 66 // from the static interval tables. The result must stay identical to 67 // runeWidthNoLUT for runes below 0x300, which TestStrictWidthLUT verifies. 68 func initStrictWidthLUTLow() { 69 for i := 0; i < 0x300; i++ { 70 r := rune(i) 71 w := byte(1) 72 if r < 0x20 || (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint 73 w = 0 74 } 75 strictWidthLUT[0][i] = w 76 } 77 78 ea := strictWidthLUT[1][:0x300] 79 fillBytes(ea, 1) 80 paint := func(t table, w byte) { 81 for _, iv := range t { 82 if iv.first >= 0x300 { 83 break 84 } 85 last := iv.last 86 if last > 0x2FF { 87 last = 0x2FF 88 } 89 fillBytes(ea[iv.first:last+1], w) 90 } 91 } 92 paint(ambiguous, 2) 93 paint(doublewidth, 2) 94 // zero-width wins over wide on overlap, so paint it last. 95 paint(combining, 0) 96 paint(nonprint, 0) 97 } 98 99 // initTables builds the merged lookup tables. It runs lazily through 100 // tablesOnce so that merely importing the package stays cheap; see issue 101 // #104. 102 func initTables() { 103 zerowidth = mergeIntervals(combining, nonprint) 104 widewidth = mergeIntervals(ambiguous, doublewidth) 105 eastAsianWidth = makeWidthTable(zerowidth, widewidth) 106 } 107 108 func mergeIntervals(t1, t2 table) table { 109 merged := make(table, 0, len(t1)+len(t2)) 110 i, j := 0, 0 111 for i < len(t1) && j < len(t2) { 112 if t1[i].first <= t2[j].first { 113 merged = append(merged, t1[i]) 114 i++ 115 } else { 116 merged = append(merged, t2[j]) 117 j++ 118 } 119 } 120 merged = append(merged, t1[i:]...) 121 merged = append(merged, t2[j:]...) 122 if len(merged) == 0 { 123 return merged 124 } 125 result := merged[:1] 126 for _, iv := range merged[1:] { 127 last := &result[len(result)-1] 128 if iv.first <= last.last+1 { 129 if iv.last > last.last { 130 last.last = iv.last 131 } 132 } else { 133 result = append(result, iv) 134 } 135 } 136 return result 137 } 138 139 func handleEnv() { 140 env := os.Getenv("RUNEWIDTH_EASTASIAN") 141 if env == "" { 142 EastAsianWidth = IsEastAsian() 143 } else { 144 EastAsianWidth = env == "1" 145 } 146 // update DefaultCondition 147 if DefaultCondition.EastAsianWidth != EastAsianWidth { 148 DefaultCondition.EastAsianWidth = EastAsianWidth 149 if len(DefaultCondition.combinedLut) > 0 { 150 DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0] 151 CreateLUT() 152 } 153 } 154 } 155 156 type interval struct { 157 first rune 158 last rune 159 } 160 161 type table []interval 162 163 type widthInterval struct { 164 first rune 165 last rune 166 width byte 167 } 168 169 type widthTable []widthInterval 170 171 func inTable(r rune, t table) bool { 172 if r < t[0].first { 173 return false 174 } 175 if r > t[len(t)-1].last { 176 return false 177 } 178 179 bot := 0 180 top := len(t) - 1 181 for top >= bot { 182 mid := (bot + top) >> 1 183 184 switch { 185 case t[mid].last < r: 186 bot = mid + 1 187 case t[mid].first > r: 188 top = mid - 1 189 default: 190 return true 191 } 192 } 193 194 return false 195 } 196 197 func makeWidthTable(zero, two table) widthTable { 198 wt := make(widthTable, 0, len(zero)+len(two)) 199 zi := 0 200 for _, iv := range two { 201 start := iv.first 202 for zi < len(zero) && zero[zi].last < start { 203 zi++ 204 } 205 for i := zi; i < len(zero) && zero[i].first <= iv.last; i++ { 206 if start < zero[i].first { 207 wt = append(wt, widthInterval{start, zero[i].first - 1, 2}) 208 } 209 if start <= zero[i].last { 210 start = zero[i].last + 1 211 } 212 if start > iv.last { 213 break 214 } 215 } 216 if start <= iv.last { 217 wt = append(wt, widthInterval{start, iv.last, 2}) 218 } 219 } 220 for _, iv := range zero { 221 wt = append(wt, widthInterval{iv.first, iv.last, 0}) 222 } 223 sort.Slice(wt, func(i, j int) bool { 224 return wt[i].first < wt[j].first 225 }) 226 return wt 227 } 228 229 func inWidthTable(r rune, t widthTable) (int, bool) { 230 if r < t[0].first { 231 return 0, false 232 } 233 if r > t[len(t)-1].last { 234 return 0, false 235 } 236 237 bot := 0 238 top := len(t) - 1 239 for top >= bot { 240 mid := (bot + top) >> 1 241 242 switch { 243 case t[mid].last < r: 244 bot = mid + 1 245 case t[mid].first > r: 246 top = mid - 1 247 default: 248 return int(t[mid].width), true 249 } 250 } 251 252 return 0, false 253 } 254 255 func runeWidthNoLUT(r rune, eastAsian, strictEmojiNeutral bool) int { 256 tablesOnce.Do(initTables) 257 if !eastAsian { 258 if r < 0x20 { 259 return 0 260 } 261 if (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint 262 return 0 263 } 264 if r < 0x300 { 265 return 1 266 } 267 switch { 268 case inTable(r, zerowidth): 269 return 0 270 case inTable(r, doublewidth): 271 return 2 272 default: 273 return 1 274 } 275 } 276 277 if r < 0x300 { 278 return int(strictWidthLUT[1][r]) 279 } 280 if w, ok := inWidthTable(r, eastAsianWidth); ok { 281 return w 282 } 283 if !strictEmojiNeutral && inTable(r, emoji) { 284 return 2 285 } 286 return 1 287 } 288 289 // fillBytes sets every byte of b to v. It doubles the copied region on each 290 // iteration so large slices are filled at memcpy speed instead of one byte 291 // per loop iteration. 292 func fillBytes(b []byte, v byte) { 293 if len(b) == 0 { 294 return 295 } 296 b[0] = v 297 for i := 1; i < len(b); i *= 2 { 298 copy(b[i:], b[:i]) 299 } 300 } 301 302 // buildStrictWidthLUT builds the strict-width lookup table above 0x300 303 // exactly once. It paints whole intervals instead of computing every rune 304 // through the binary searches in runeWidthNoLUT. It must not write below 305 // 0x300: that region was filled by init and may be read concurrently. The 306 // result must stay identical to runeWidthNoLUT(r, eastAsian, true), which 307 // TestStrictWidthLUT verifies. 308 func buildStrictWidthLUT() { 309 strictWidthLUTOnce.Do(func() { 310 tablesOnce.Do(initTables) 311 312 // paintHigh fills lut with w over each interval, clipped to 0x300+. 313 paintHigh := func(lut []byte, first, last rune, w byte) { 314 if first < 0x300 { 315 if last < 0x300 { 316 return 317 } 318 first = 0x300 319 } 320 fillBytes(lut[first:last+1], w) 321 } 322 323 // EastAsianWidth=false, StrictEmojiNeutral=true 324 lut := strictWidthLUT[0][:] 325 fillBytes(lut[0x300:], 1) 326 for _, iv := range doublewidth { 327 paintHigh(lut, iv.first, iv.last, 2) 328 } 329 // zerowidth is checked before doublewidth, so it wins on overlap. 330 for _, iv := range zerowidth { 331 paintHigh(lut, iv.first, iv.last, 0) 332 } 333 334 // EastAsianWidth=true, StrictEmojiNeutral=true 335 lut = strictWidthLUT[1][:] 336 fillBytes(lut[0x300:], 1) 337 for _, iv := range eastAsianWidth { 338 paintHigh(lut, iv.first, iv.last, iv.width) 339 } 340 341 strictWidthLUTLimit.Store(0x110000) 342 }) 343 } 344 345 var private = table{ 346 {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD}, 347 } 348 349 var nonprint = table{ 350 {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, 351 {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, 352 {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, 353 {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, 354 } 355 356 // Condition have flag EastAsianWidth whether the current locale is CJK or not. 357 type Condition struct { 358 combinedLut []byte 359 EastAsianWidth bool 360 StrictEmojiNeutral bool 361 362 // Deprecated: ZWJ sequences are always handled through Unicode 363 // grapheme cluster segmentation now, so this flag has no effect. 364 // It is kept only for compatibility with code written against 365 // v0.0.9 and earlier. 366 ZeroWidthJoiner bool 367 } 368 369 // NewCondition return new instance of Condition which is current locale. 370 func NewCondition() *Condition { 371 return &Condition{ 372 EastAsianWidth: EastAsianWidth, 373 StrictEmojiNeutral: StrictEmojiNeutral, 374 ZeroWidthJoiner: ZeroWidthJoiner, 375 } 376 } 377 378 // RuneWidth returns the number of cells in r. 379 // See http://www.unicode.org/reports/tr11/ 380 func (c *Condition) RuneWidth(r rune) int { 381 // This one compare doubles as the range check and the lazy-LUT check: 382 // out-of-range runes and runes above the built portion of 383 // strictWidthLUT both take the slow path. Once the LUT is fully built 384 // the limit is 0x110000 and only invalid runes go slow. 385 if uint32(r) >= uint32(strictWidthLUTLimit.Load()) { 386 return c.runeWidthSlow(r) 387 } 388 if len(c.combinedLut) > 0 { 389 return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3 390 } 391 if c.StrictEmojiNeutral { 392 if c.EastAsianWidth { 393 return int(strictWidthLUT[1][r]) 394 } 395 return int(strictWidthLUT[0][r]) 396 } 397 return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral) 398 } 399 400 func (c *Condition) runeWidthSlow(r rune) int { 401 if r < 0 || r > 0x10FFFF { 402 return 0 403 } 404 buildStrictWidthLUT() 405 if len(c.combinedLut) > 0 { 406 return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3 407 } 408 if c.StrictEmojiNeutral { 409 if c.EastAsianWidth { 410 return int(strictWidthLUT[1][r]) 411 } 412 return int(strictWidthLUT[0][r]) 413 } 414 return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral) 415 } 416 417 // CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation. 418 // This should not be called concurrently with other operations on c. 419 // If options in c is changed, CreateLUT should be called again. 420 func (c *Condition) CreateLUT() { 421 const max = 0x110000 422 lut := c.combinedLut 423 if len(c.combinedLut) != 0 { 424 // Remove so we don't use it. 425 c.combinedLut = nil 426 } else { 427 lut = make([]byte, max/2) 428 } 429 for i := range lut { 430 i32 := int32(i * 2) 431 x0 := c.RuneWidth(i32) 432 x1 := c.RuneWidth(i32 + 1) 433 lut[i] = uint8(x0) | uint8(x1)<<4 434 } 435 c.combinedLut = lut 436 } 437 438 // graphemeWidth returns the width of a single grapheme cluster: the sum of 439 // the widths of its runes, capped at 2 cells. The cap keeps multi-rune 440 // sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo) 441 // from being counted wider than the two cells terminals give them. 442 func (c *Condition) graphemeWidth(cluster string) int { 443 width := 0 444 for _, r := range cluster { 445 width += c.RuneWidth(r) 446 } 447 if width > 2 { 448 width = 2 449 } 450 return width 451 } 452 453 // StringWidth return width as you can see 454 func (c *Condition) StringWidth(s string) (width int) { 455 if len(s) == 1 { 456 b := s[0] 457 if b < 0x20 || b == 0x7F { 458 return 0 459 } 460 return 1 461 } 462 if len(s) > 0 && len(s) <= utf8.UTFMax { 463 r, size := utf8.DecodeRuneInString(s) 464 if size == len(s) { 465 return c.RuneWidth(r) 466 } 467 } 468 // ASCII fast path: no grapheme clustering needed for pure ASCII 469 for i := 0; i < len(s); i++ { 470 b := s[i] 471 if b >= 0x80 { 472 goto graphemes 473 } 474 if b >= 0x20 && b != 0x7F { 475 width++ 476 } 477 } 478 return 479 480 graphemes: 481 width = 0 482 g := graphemes.FromString(s) 483 for g.Next() { 484 width += c.graphemeWidth(g.Value()) 485 } 486 return 487 } 488 489 // Truncate return string truncated with w cells 490 func (c *Condition) Truncate(s string, w int, tail string) string { 491 if c.StringWidth(s) <= w { 492 return s 493 } 494 w -= c.StringWidth(tail) 495 var width int 496 pos := len(s) 497 g := graphemes.FromString(s) 498 for g.Next() { 499 chWidth := c.graphemeWidth(g.Value()) 500 if width+chWidth > w { 501 pos = g.Start() 502 break 503 } 504 width += chWidth 505 } 506 return s[:pos] + tail 507 } 508 509 // TruncateLeft cuts w cells from the beginning of the `s`. 510 func (c *Condition) TruncateLeft(s string, w int, prefix string) string { 511 if c.StringWidth(s) <= w { 512 return prefix 513 } 514 515 var width int 516 pos := len(s) 517 518 g := graphemes.FromString(s) 519 for g.Next() { 520 chWidth := c.graphemeWidth(g.Value()) 521 522 if width+chWidth > w { 523 if width < w { 524 pos = g.End() 525 prefix += strings.Repeat(" ", width+chWidth-w) 526 } else { 527 pos = g.Start() 528 } 529 530 break 531 } 532 533 width += chWidth 534 } 535 536 return prefix + s[pos:] 537 } 538 539 // TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended 540 func (c *Condition) TruncatePrefix(s string, w int, prefix string) string { 541 if c.StringWidth(prefix) >= w { 542 return prefix 543 } 544 545 sw := c.StringWidth(s) 546 if sw <= w { 547 return s 548 } 549 w -= c.StringWidth(prefix) 550 var width int 551 var pos int 552 g := graphemes.FromString(s) 553 for g.Next() { 554 chWidth := c.graphemeWidth(g.Value()) 555 if sw-(width+chWidth) <= w { 556 pos = g.End() 557 break 558 } 559 width += chWidth 560 } 561 562 return prefix + s[pos:] 563 } 564 565 // Wrap return string wrapped with w cells 566 func (c *Condition) Wrap(s string, w int) string { 567 width := 0 568 var out strings.Builder 569 out.Grow(len(s) + len(s)/w + 1) 570 for _, r := range s { 571 cw := c.RuneWidth(r) 572 if r == '\n' { 573 out.WriteRune(r) 574 width = 0 575 continue 576 } else if width+cw > w { 577 out.WriteByte('\n') 578 width = 0 579 out.WriteRune(r) 580 width += cw 581 continue 582 } 583 out.WriteRune(r) 584 width += cw 585 } 586 return out.String() 587 } 588 589 // FillLeft return string filled in left by spaces in w cells 590 func (c *Condition) FillLeft(s string, w int) string { 591 width := c.StringWidth(s) 592 count := w - width 593 if count > 0 { 594 return strings.Repeat(" ", count) + s 595 } 596 return s 597 } 598 599 // FillRight return string filled in left by spaces in w cells 600 func (c *Condition) FillRight(s string, w int) string { 601 width := c.StringWidth(s) 602 count := w - width 603 if count > 0 { 604 return s + strings.Repeat(" ", count) 605 } 606 return s 607 } 608 609 // RuneWidth returns the number of cells in r. 610 // See http://www.unicode.org/reports/tr11/ 611 func RuneWidth(r rune) int { 612 return DefaultCondition.RuneWidth(r) 613 } 614 615 // IsAmbiguousWidth returns whether is ambiguous width or not. 616 func IsAmbiguousWidth(r rune) bool { 617 return inTable(r, private) || inTable(r, ambiguous) 618 } 619 620 // IsCombiningWidth returns whether is combining width or not. 621 func IsCombiningWidth(r rune) bool { 622 return inTable(r, combining) 623 } 624 625 // IsNeutralWidth returns whether is neutral width or not. 626 func IsNeutralWidth(r rune) bool { 627 return inTable(r, neutral) 628 } 629 630 // StringWidth return width as you can see 631 func StringWidth(s string) (width int) { 632 return DefaultCondition.StringWidth(s) 633 } 634 635 // Truncate return string truncated with w cells 636 func Truncate(s string, w int, tail string) string { 637 return DefaultCondition.Truncate(s, w, tail) 638 } 639 640 // TruncateLeft cuts w cells from the beginning of the `s`. 641 func TruncateLeft(s string, w int, prefix string) string { 642 return DefaultCondition.TruncateLeft(s, w, prefix) 643 } 644 645 // TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended 646 func TruncatePrefix(s string, w int, prefix string) string { 647 return DefaultCondition.TruncatePrefix(s, w, prefix) 648 } 649 650 // Wrap return string wrapped with w cells 651 func Wrap(s string, w int) string { 652 return DefaultCondition.Wrap(s, w) 653 } 654 655 // FillLeft return string filled in left by spaces in w cells 656 func FillLeft(s string, w int) string { 657 return DefaultCondition.FillLeft(s, w) 658 } 659 660 // FillRight return string filled in left by spaces in w cells 661 func FillRight(s string, w int) string { 662 return DefaultCondition.FillRight(s, w) 663 } 664 665 // CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation. 666 // This should not be called concurrently with other operations. 667 func CreateLUT() { 668 if len(DefaultCondition.combinedLut) > 0 { 669 return 670 } 671 DefaultCondition.CreateLUT() 672 }