1package runewidth 2 3import ( 4 "os" 5 6 "github.com/rivo/uniseg" 7) 8 9//go:generate go run script/generate.go 10 11var ( 12 // EastAsianWidth will be set true if the current locale is CJK 13 EastAsianWidth bool 14 15 // StrictEmojiNeutral should be set false if handle broken fonts 16 StrictEmojiNeutral bool = true 17 18 // DefaultCondition is a condition in current locale 19 DefaultCondition = &Condition{ 20 EastAsianWidth: false, 21 StrictEmojiNeutral: true, 22 } 23) 24 25func init() { 26 handleEnv() 27} 28 29func handleEnv() { 30 env := os.Getenv("RUNEWIDTH_EASTASIAN") 31 if env == "" { 32 EastAsianWidth = IsEastAsian() 33 } else { 34 EastAsianWidth = env == "1" 35 } 36 // update DefaultCondition 37 DefaultCondition.EastAsianWidth = EastAsianWidth 38} 39 40type interval struct { 41 first rune 42 last rune 43} 44 45type table []interval 46 47func inTables(r rune, ts ...table) bool { 48 for _, t := range ts { 49 if inTable(r, t) { 50 return true 51 } 52 } 53 return false 54} 55 56func inTable(r rune, t table) bool { 57 if r < t[0].first { 58 return false 59 } 60 61 bot := 0 62 top := len(t) - 1 63 for top >= bot { 64 mid := (bot + top) >> 1 65 66 switch { 67 case t[mid].last < r: 68 bot = mid + 1 69 case t[mid].first > r: 70 top = mid - 1 71 default: 72 return true 73 } 74 } 75 76 return false 77} 78 79var private = table{ 80 {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD}, 81} 82 83var nonprint = table{ 84 {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, 85 {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, 86 {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, 87 {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, 88} 89 90// Condition have flag EastAsianWidth whether the current locale is CJK or not. 91type Condition struct { 92 EastAsianWidth bool 93 StrictEmojiNeutral bool 94} 95 96// NewCondition return new instance of Condition which is current locale. 97func NewCondition() *Condition { 98 return &Condition{ 99 EastAsianWidth: EastAsianWidth, 100 StrictEmojiNeutral: StrictEmojiNeutral, 101 } 102} 103 104// RuneWidth returns the number of cells in r. 105// See http://www.unicode.org/reports/tr11/ 106func (c *Condition) RuneWidth(r rune) int { 107 // optimized version, verified by TestRuneWidthChecksums() 108 if !c.EastAsianWidth { 109 switch { 110 case r < 0x20 || r > 0x10FFFF: 111 return 0 112 case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint 113 return 0 114 case r < 0x300: 115 return 1 116 case inTable(r, narrow): 117 return 1 118 case inTables(r, nonprint, combining): 119 return 0 120 case inTable(r, doublewidth): 121 return 2 122 default: 123 return 1 124 } 125 } else { 126 switch { 127 case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining): 128 return 0 129 case inTable(r, narrow): 130 return 1 131 case inTables(r, ambiguous, doublewidth): 132 return 2 133 case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow): 134 return 2 135 default: 136 return 1 137 } 138 } 139} 140 141// StringWidth return width as you can see 142func (c *Condition) StringWidth(s string) (width int) { 143 g := uniseg.NewGraphemes(s) 144 for g.Next() { 145 var chWidth int 146 for _, r := range g.Runes() { 147 chWidth = c.RuneWidth(r) 148 if chWidth > 0 { 149 break // Our best guess at this point is to use the width of the first non-zero-width rune. 150 } 151 } 152 width += chWidth 153 } 154 return 155} 156 157// Truncate return string truncated with w cells 158func (c *Condition) Truncate(s string, w int, tail string) string { 159 if c.StringWidth(s) <= w { 160 return s 161 } 162 w -= c.StringWidth(tail) 163 var width int 164 pos := len(s) 165 g := uniseg.NewGraphemes(s) 166 for g.Next() { 167 var chWidth int 168 for _, r := range g.Runes() { 169 chWidth = c.RuneWidth(r) 170 if chWidth > 0 { 171 break // See StringWidth() for details. 172 } 173 } 174 if width+chWidth > w { 175 pos, _ = g.Positions() 176 break 177 } 178 width += chWidth 179 } 180 return s[:pos] + tail 181} 182 183// Wrap return string wrapped with w cells 184func (c *Condition) Wrap(s string, w int) string { 185 width := 0 186 out := "" 187 for _, r := range []rune(s) { 188 cw := c.RuneWidth(r) 189 if r == '\n' { 190 out += string(r) 191 width = 0 192 continue 193 } else if width+cw > w { 194 out += "\n" 195 width = 0 196 out += string(r) 197 width += cw 198 continue 199 } 200 out += string(r) 201 width += cw 202 } 203 return out 204} 205 206// FillLeft return string filled in left by spaces in w cells 207func (c *Condition) FillLeft(s string, w int) string { 208 width := c.StringWidth(s) 209 count := w - width 210 if count > 0 { 211 b := make([]byte, count) 212 for i := range b { 213 b[i] = ' ' 214 } 215 return string(b) + s 216 } 217 return s 218} 219 220// FillRight return string filled in left by spaces in w cells 221func (c *Condition) FillRight(s string, w int) string { 222 width := c.StringWidth(s) 223 count := w - width 224 if count > 0 { 225 b := make([]byte, count) 226 for i := range b { 227 b[i] = ' ' 228 } 229 return s + string(b) 230 } 231 return s 232} 233 234// RuneWidth returns the number of cells in r. 235// See http://www.unicode.org/reports/tr11/ 236func RuneWidth(r rune) int { 237 return DefaultCondition.RuneWidth(r) 238} 239 240// IsAmbiguousWidth returns whether is ambiguous width or not. 241func IsAmbiguousWidth(r rune) bool { 242 return inTables(r, private, ambiguous) 243} 244 245// IsNeutralWidth returns whether is neutral width or not. 246func IsNeutralWidth(r rune) bool { 247 return inTable(r, neutral) 248} 249 250// StringWidth return width as you can see 251func StringWidth(s string) (width int) { 252 return DefaultCondition.StringWidth(s) 253} 254 255// Truncate return string truncated with w cells 256func Truncate(s string, w int, tail string) string { 257 return DefaultCondition.Truncate(s, w, tail) 258} 259 260// Wrap return string wrapped with w cells 261func Wrap(s string, w int) string { 262 return DefaultCondition.Wrap(s, w) 263} 264 265// FillLeft return string filled in left by spaces in w cells 266func FillLeft(s string, w int) string { 267 return DefaultCondition.FillLeft(s, w) 268} 269 270// FillRight return string filled in left by spaces in w cells 271func FillRight(s string, w int) string { 272 return DefaultCondition.FillRight(s, w) 273} 274