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