Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
4.9 KiB
241 lines
4.9 KiB
package runewidth |
|
|
|
import ( |
|
"os" |
|
|
|
"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 |
|
|
|
// 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 |
|
} |
|
|
|
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 { |
|
EastAsianWidth bool |
|
} |
|
|
|
// NewCondition return new instance of Condition which is current locale. |
|
func NewCondition() *Condition { |
|
return &Condition{ |
|
EastAsianWidth: EastAsianWidth, |
|
} |
|
} |
|
|
|
// 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 |
|
} |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// 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 := 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) |
|
} |
|
|
|
// 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) |
|
}
|
|
|