src

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

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 }