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