src

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

runewidth.go (7712B)


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