1package runewidth
  2
  3import (
  4	"os"
  5)
  6
  7//go:generate go run script/generate.go
  8
  9var (
 10	// EastAsianWidth will be set true if the current locale is CJK
 11	EastAsianWidth bool
 12
 13	// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ
 14	ZeroWidthJoiner bool
 15
 16	// DefaultCondition is a condition in current locale
 17	DefaultCondition = &Condition{}
 18)
 19
 20func init() {
 21	handleEnv()
 22}
 23
 24func handleEnv() {
 25	env := os.Getenv("RUNEWIDTH_EASTASIAN")
 26	if env == "" {
 27		EastAsianWidth = IsEastAsian()
 28	} else {
 29		EastAsianWidth = env == "1"
 30	}
 31	// update DefaultCondition
 32	DefaultCondition.EastAsianWidth = EastAsianWidth
 33	DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
 34}
 35
 36type interval struct {
 37	first rune
 38	last  rune
 39}
 40
 41type table []interval
 42
 43func inTables(r rune, ts ...table) bool {
 44	for _, t := range ts {
 45		if inTable(r, t) {
 46			return true
 47		}
 48	}
 49	return false
 50}
 51
 52func inTable(r rune, t table) bool {
 53	if r < t[0].first {
 54		return false
 55	}
 56
 57	bot := 0
 58	top := len(t) - 1
 59	for top >= bot {
 60		mid := (bot + top) >> 1
 61
 62		switch {
 63		case t[mid].last < r:
 64			bot = mid + 1
 65		case t[mid].first > r:
 66			top = mid - 1
 67		default:
 68			return true
 69		}
 70	}
 71
 72	return false
 73}
 74
 75var private = table{
 76	{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
 77}
 78
 79var nonprint = table{
 80	{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
 81	{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
 82	{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
 83	{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
 84}
 85
 86// Condition have flag EastAsianWidth whether the current locale is CJK or not.
 87type Condition struct {
 88	EastAsianWidth  bool
 89	ZeroWidthJoiner bool
 90}
 91
 92// NewCondition return new instance of Condition which is current locale.
 93func NewCondition() *Condition {
 94	return &Condition{
 95		EastAsianWidth:  EastAsianWidth,
 96		ZeroWidthJoiner: ZeroWidthJoiner,
 97	}
 98}
 99
100// RuneWidth returns the number of cells in r.
101// See http://www.unicode.org/reports/tr11/
102func (c *Condition) RuneWidth(r rune) int {
103	switch {
104	case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
105		return 0
106	case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
107		return 2
108	default:
109		return 1
110	}
111}
112
113func (c *Condition) stringWidth(s string) (width int) {
114	for _, r := range []rune(s) {
115		width += c.RuneWidth(r)
116	}
117	return width
118}
119
120func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
121	r1, r2 := rune(0), rune(0)
122	for _, r := range []rune(s) {
123		if r == 0xFE0E || r == 0xFE0F {
124			continue
125		}
126		w := c.RuneWidth(r)
127		if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
128			if width < w {
129				width = w
130			}
131		} else {
132			width += w
133		}
134		r1, r2 = r2, r
135	}
136	return width
137}
138
139// StringWidth return width as you can see
140func (c *Condition) StringWidth(s string) (width int) {
141	if c.ZeroWidthJoiner {
142		return c.stringWidthZeroJoiner(s)
143	}
144	return c.stringWidth(s)
145}
146
147// Truncate return string truncated with w cells
148func (c *Condition) Truncate(s string, w int, tail string) string {
149	if c.StringWidth(s) <= w {
150		return s
151	}
152	r := []rune(s)
153	tw := c.StringWidth(tail)
154	w -= tw
155	width := 0
156	i := 0
157	for ; i < len(r); i++ {
158		cw := c.RuneWidth(r[i])
159		if width+cw > w {
160			break
161		}
162		width += cw
163	}
164	return string(r[0:i]) + tail
165}
166
167// Wrap return string wrapped with w cells
168func (c *Condition) Wrap(s string, w int) string {
169	width := 0
170	out := ""
171	for _, r := range []rune(s) {
172		cw := RuneWidth(r)
173		if r == '\n' {
174			out += string(r)
175			width = 0
176			continue
177		} else if width+cw > w {
178			out += "\n"
179			width = 0
180			out += string(r)
181			width += cw
182			continue
183		}
184		out += string(r)
185		width += cw
186	}
187	return out
188}
189
190// FillLeft return string filled in left by spaces in w cells
191func (c *Condition) FillLeft(s string, w int) string {
192	width := c.StringWidth(s)
193	count := w - width
194	if count > 0 {
195		b := make([]byte, count)
196		for i := range b {
197			b[i] = ' '
198		}
199		return string(b) + s
200	}
201	return s
202}
203
204// FillRight return string filled in left by spaces in w cells
205func (c *Condition) FillRight(s string, w int) string {
206	width := c.StringWidth(s)
207	count := w - width
208	if count > 0 {
209		b := make([]byte, count)
210		for i := range b {
211			b[i] = ' '
212		}
213		return s + string(b)
214	}
215	return s
216}
217
218// RuneWidth returns the number of cells in r.
219// See http://www.unicode.org/reports/tr11/
220func RuneWidth(r rune) int {
221	return DefaultCondition.RuneWidth(r)
222}
223
224// IsAmbiguousWidth returns whether is ambiguous width or not.
225func IsAmbiguousWidth(r rune) bool {
226	return inTables(r, private, ambiguous)
227}
228
229// IsNeutralWidth returns whether is neutral width or not.
230func IsNeutralWidth(r rune) bool {
231	return inTable(r, neutral)
232}
233
234// StringWidth return width as you can see
235func StringWidth(s string) (width int) {
236	return DefaultCondition.StringWidth(s)
237}
238
239// Truncate return string truncated with w cells
240func Truncate(s string, w int, tail string) string {
241	return DefaultCondition.Truncate(s, w, tail)
242}
243
244// Wrap return string wrapped with w cells
245func Wrap(s string, w int) string {
246	return DefaultCondition.Wrap(s, w)
247}
248
249// FillLeft return string filled in left by spaces in w cells
250func FillLeft(s string, w int) string {
251	return DefaultCondition.FillLeft(s, w)
252}
253
254// FillRight return string filled in left by spaces in w cells
255func FillRight(s string, w int) string {
256	return DefaultCondition.FillRight(s, w)
257}