src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }