2022-04-25 06:00:55 -04:00
|
|
|
package runewidth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
2023-05-03 11:07:09 -04:00
|
|
|
"strings"
|
2022-04-25 06:00:55 -04:00
|
|
|
|
|
|
|
"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
|
2023-05-03 11:07:09 -04:00
|
|
|
if DefaultCondition.EastAsianWidth != EastAsianWidth {
|
|
|
|
DefaultCondition.EastAsianWidth = EastAsianWidth
|
|
|
|
if len(DefaultCondition.combinedLut) > 0 {
|
|
|
|
DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
|
|
|
|
CreateLUT()
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 06:00:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-05-03 11:07:09 -04:00
|
|
|
combinedLut []byte
|
2022-04-25 06:00:55 -04:00
|
|
|
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 {
|
2023-05-03 11:07:09 -04:00
|
|
|
if r < 0 || r > 0x10FFFF {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if len(c.combinedLut) > 0 {
|
|
|
|
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
|
|
|
|
}
|
2022-04-25 06:00:55 -04:00
|
|
|
// optimized version, verified by TestRuneWidthChecksums()
|
|
|
|
if !c.EastAsianWidth {
|
|
|
|
switch {
|
2023-05-03 11:07:09 -04:00
|
|
|
case r < 0x20:
|
2022-04-25 06:00:55 -04:00
|
|
|
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 {
|
2023-05-03 11:07:09 -04:00
|
|
|
case inTables(r, nonprint, combining):
|
2022-04-25 06:00:55 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:07:09 -04:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:00:55 -04:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:07:09 -04:00
|
|
|
// 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:]
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:00:55 -04:00
|
|
|
// Wrap return string wrapped with w cells
|
|
|
|
func (c *Condition) Wrap(s string, w int) string {
|
|
|
|
width := 0
|
|
|
|
out := ""
|
2023-05-03 11:07:09 -04:00
|
|
|
for _, r := range s {
|
2022-04-25 06:00:55 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:07:09 -04:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:00:55 -04:00
|
|
|
// 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)
|
|
|
|
}
|
2023-05-03 11:07:09 -04:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|