| package runewidth |
| |
| import ( |
| "os" |
| ) |
| |
| //go:generate go run script/generate.go |
| |
| var ( |
| // EastAsianWidth will be set true if the current locale is CJK |
| EastAsianWidth bool |
| |
| // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ |
| ZeroWidthJoiner bool |
| |
| // DefaultCondition is a condition in current locale |
| DefaultCondition = &Condition{} |
| ) |
| |
| func init() { |
| handleEnv() |
| } |
| |
| func handleEnv() { |
| env := os.Getenv("RUNEWIDTH_EASTASIAN") |
| if env == "" { |
| EastAsianWidth = IsEastAsian() |
| } else { |
| EastAsianWidth = env == "1" |
| } |
| // update DefaultCondition |
| DefaultCondition.EastAsianWidth = EastAsianWidth |
| DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner |
| } |
| |
| 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 { |
| // func (t table) IncludesRune(r rune) 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 { |
| EastAsianWidth bool |
| ZeroWidthJoiner bool |
| } |
| |
| // NewCondition return new instance of Condition which is current locale. |
| func NewCondition() *Condition { |
| return &Condition{ |
| EastAsianWidth: EastAsianWidth, |
| ZeroWidthJoiner: ZeroWidthJoiner, |
| } |
| } |
| |
| // RuneWidth returns the number of cells in r. |
| // See http://www.unicode.org/reports/tr11/ |
| func (c *Condition) RuneWidth(r rune) int { |
| switch { |
| case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned): |
| return 0 |
| case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth): |
| return 2 |
| default: |
| return 1 |
| } |
| } |
| |
| func (c *Condition) stringWidth(s string) (width int) { |
| for _, r := range []rune(s) { |
| width += c.RuneWidth(r) |
| } |
| return width |
| } |
| |
| func (c *Condition) stringWidthZeroJoiner(s string) (width int) { |
| r1, r2 := rune(0), rune(0) |
| for _, r := range []rune(s) { |
| if r == 0xFE0E || r == 0xFE0F { |
| continue |
| } |
| w := c.RuneWidth(r) |
| if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) { |
| if width < w { |
| width = w |
| } |
| } else { |
| width += w |
| } |
| r1, r2 = r2, r |
| } |
| return width |
| } |
| |
| // StringWidth return width as you can see |
| func (c *Condition) StringWidth(s string) (width int) { |
| if c.ZeroWidthJoiner { |
| return c.stringWidthZeroJoiner(s) |
| } |
| return c.stringWidth(s) |
| } |
| |
| // 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 |
| } |
| r := []rune(s) |
| tw := c.StringWidth(tail) |
| w -= tw |
| width := 0 |
| i := 0 |
| for ; i < len(r); i++ { |
| cw := c.RuneWidth(r[i]) |
| if width+cw > w { |
| break |
| } |
| width += cw |
| } |
| return string(r[0:i]) + tail |
| } |
| |
| // Wrap return string wrapped with w cells |
| func (c *Condition) Wrap(s string, w int) string { |
| width := 0 |
| out := "" |
| for _, r := range []rune(s) { |
| cw := 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) |
| } |
| |
| // 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) |
| } |