123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- package runewidth
- import (
- "os"
- "strings"
- "github.com/rivo/uniseg"
- )
- //go:generate go run script/generate.go
- var (
- // EastAsianWidth will be set true if the current locale is CJK
- EastAsianWidth bool
- // StrictEmojiNeutral should be set false if handle broken fonts
- StrictEmojiNeutral bool = true
- // DefaultCondition is a condition in current locale
- DefaultCondition = &Condition{
- EastAsianWidth: false,
- StrictEmojiNeutral: true,
- }
- )
- func init() {
- handleEnv()
- }
- func handleEnv() {
- env := os.Getenv("RUNEWIDTH_EASTASIAN")
- if env == "" {
- EastAsianWidth = IsEastAsian()
- } else {
- EastAsianWidth = env == "1"
- }
- // update DefaultCondition
- if DefaultCondition.EastAsianWidth != EastAsianWidth {
- DefaultCondition.EastAsianWidth = EastAsianWidth
- if len(DefaultCondition.combinedLut) > 0 {
- DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
- CreateLUT()
- }
- }
- }
- type interval struct {
- first rune
- last rune
- }
- type table []interval
- func inTables(r rune, ts ...table) bool {
- for _, t := range ts {
- if inTable(r, t) {
- return true
- }
- }
- return false
- }
- func inTable(r rune, t table) bool {
- if r < t[0].first {
- return false
- }
- bot := 0
- top := len(t) - 1
- for top >= bot {
- mid := (bot + top) >> 1
- switch {
- case t[mid].last < r:
- bot = mid + 1
- case t[mid].first > r:
- top = mid - 1
- default:
- return true
- }
- }
- return false
- }
- var private = table{
- {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
- }
- var nonprint = table{
- {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
- {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
- {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
- {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
- }
- // Condition have flag EastAsianWidth whether the current locale is CJK or not.
- type Condition struct {
- combinedLut []byte
- EastAsianWidth bool
- StrictEmojiNeutral bool
- }
- // NewCondition return new instance of Condition which is current locale.
- func NewCondition() *Condition {
- return &Condition{
- EastAsianWidth: EastAsianWidth,
- StrictEmojiNeutral: StrictEmojiNeutral,
- }
- }
- // RuneWidth returns the number of cells in r.
- // See http://www.unicode.org/reports/tr11/
- func (c *Condition) RuneWidth(r rune) int {
- if r < 0 || r > 0x10FFFF {
- return 0
- }
- if len(c.combinedLut) > 0 {
- return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
- }
- // optimized version, verified by TestRuneWidthChecksums()
- if !c.EastAsianWidth {
- switch {
- case r < 0x20:
- return 0
- case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
- return 0
- case r < 0x300:
- return 1
- case inTable(r, narrow):
- return 1
- case inTables(r, nonprint, combining):
- return 0
- case inTable(r, doublewidth):
- return 2
- default:
- return 1
- }
- } else {
- switch {
- case inTables(r, nonprint, combining):
- return 0
- case inTable(r, narrow):
- return 1
- case inTables(r, ambiguous, doublewidth):
- return 2
- case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow):
- return 2
- default:
- return 1
- }
- }
- }
- // CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
- // This should not be called concurrently with other operations on c.
- // If options in c is changed, CreateLUT should be called again.
- func (c *Condition) CreateLUT() {
- const max = 0x110000
- lut := c.combinedLut
- if len(c.combinedLut) != 0 {
- // Remove so we don't use it.
- c.combinedLut = nil
- } else {
- lut = make([]byte, max/2)
- }
- for i := range lut {
- i32 := int32(i * 2)
- x0 := c.RuneWidth(i32)
- x1 := c.RuneWidth(i32 + 1)
- lut[i] = uint8(x0) | uint8(x1)<<4
- }
- c.combinedLut = lut
- }
- // StringWidth return width as you can see
- func (c *Condition) StringWidth(s string) (width int) {
- g := uniseg.NewGraphemes(s)
- for g.Next() {
- var chWidth int
- for _, r := range g.Runes() {
- chWidth = c.RuneWidth(r)
- if chWidth > 0 {
- break // Our best guess at this point is to use the width of the first non-zero-width rune.
- }
- }
- width += chWidth
- }
- return
- }
- // Truncate return string truncated with w cells
- func (c *Condition) Truncate(s string, w int, tail string) string {
- if c.StringWidth(s) <= w {
- return s
- }
- w -= c.StringWidth(tail)
- var width int
- pos := len(s)
- g := uniseg.NewGraphemes(s)
- for g.Next() {
- var chWidth int
- for _, r := range g.Runes() {
- chWidth = c.RuneWidth(r)
- if chWidth > 0 {
- break // See StringWidth() for details.
- }
- }
- if width+chWidth > w {
- pos, _ = g.Positions()
- break
- }
- width += chWidth
- }
- return s[:pos] + tail
- }
- // TruncateLeft cuts w cells from the beginning of the `s`.
- func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
- if c.StringWidth(s) <= w {
- return prefix
- }
- var width int
- pos := len(s)
- g := uniseg.NewGraphemes(s)
- for g.Next() {
- var chWidth int
- for _, r := range g.Runes() {
- chWidth = c.RuneWidth(r)
- if chWidth > 0 {
- break // See StringWidth() for details.
- }
- }
- if width+chWidth > w {
- if width < w {
- _, pos = g.Positions()
- prefix += strings.Repeat(" ", width+chWidth-w)
- } else {
- pos, _ = g.Positions()
- }
- break
- }
- width += chWidth
- }
- return prefix + s[pos:]
- }
- // Wrap return string wrapped with w cells
- func (c *Condition) Wrap(s string, w int) string {
- width := 0
- out := ""
- for _, r := range s {
- cw := c.RuneWidth(r)
- if r == '\n' {
- out += string(r)
- width = 0
- continue
- } else if width+cw > w {
- out += "\n"
- width = 0
- out += string(r)
- width += cw
- continue
- }
- out += string(r)
- width += cw
- }
- return out
- }
- // FillLeft return string filled in left by spaces in w cells
- func (c *Condition) FillLeft(s string, w int) string {
- width := c.StringWidth(s)
- count := w - width
- if count > 0 {
- b := make([]byte, count)
- for i := range b {
- b[i] = ' '
- }
- return string(b) + s
- }
- return s
- }
- // FillRight return string filled in left by spaces in w cells
- func (c *Condition) FillRight(s string, w int) string {
- width := c.StringWidth(s)
- count := w - width
- if count > 0 {
- b := make([]byte, count)
- for i := range b {
- b[i] = ' '
- }
- return s + string(b)
- }
- return s
- }
- // RuneWidth returns the number of cells in r.
- // See http://www.unicode.org/reports/tr11/
- func RuneWidth(r rune) int {
- return DefaultCondition.RuneWidth(r)
- }
- // IsAmbiguousWidth returns whether is ambiguous width or not.
- func IsAmbiguousWidth(r rune) bool {
- return inTables(r, private, ambiguous)
- }
- // IsNeutralWidth returns whether is neutral width or not.
- func IsNeutralWidth(r rune) bool {
- return inTable(r, neutral)
- }
- // StringWidth return width as you can see
- func StringWidth(s string) (width int) {
- return DefaultCondition.StringWidth(s)
- }
- // Truncate return string truncated with w cells
- func Truncate(s string, w int, tail string) string {
- return DefaultCondition.Truncate(s, w, tail)
- }
- // TruncateLeft cuts w cells from the beginning of the `s`.
- func TruncateLeft(s string, w int, prefix string) string {
- return DefaultCondition.TruncateLeft(s, w, prefix)
- }
- // Wrap return string wrapped with w cells
- func Wrap(s string, w int) string {
- return DefaultCondition.Wrap(s, w)
- }
- // FillLeft return string filled in left by spaces in w cells
- func FillLeft(s string, w int) string {
- return DefaultCondition.FillLeft(s, w)
- }
- // FillRight return string filled in left by spaces in w cells
- func FillRight(s string, w int) string {
- return DefaultCondition.FillRight(s, w)
- }
- // CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
- // This should not be called concurrently with other operations.
- func CreateLUT() {
- if len(DefaultCondition.combinedLut) > 0 {
- return
- }
- DefaultCondition.CreateLUT()
- }
|