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  }