runewidth.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. package runewidth
  2. import (
  3. "os"
  4. "strings"
  5. "unicode/utf8"
  6. "github.com/clipperhouse/uax29/v2/graphemes"
  7. )
  8. //go:generate go run script/generate.go
  9. var (
  10. // EastAsianWidth will be set true if the current locale is CJK
  11. EastAsianWidth bool
  12. // StrictEmojiNeutral should be set false if handle broken fonts
  13. StrictEmojiNeutral bool = true
  14. // DefaultCondition is a condition in current locale
  15. DefaultCondition = &Condition{
  16. EastAsianWidth: false,
  17. StrictEmojiNeutral: true,
  18. }
  19. )
  20. var (
  21. zerowidth table // combining + nonprint merged for faster zero-width lookup
  22. widewidth table // ambiguous + doublewidth merged for EA path
  23. )
  24. func init() {
  25. zerowidth = mergeIntervals(combining, nonprint)
  26. widewidth = mergeIntervals(ambiguous, doublewidth)
  27. handleEnv()
  28. }
  29. func mergeIntervals(t1, t2 table) table {
  30. merged := make(table, 0, len(t1)+len(t2))
  31. i, j := 0, 0
  32. for i < len(t1) && j < len(t2) {
  33. if t1[i].first <= t2[j].first {
  34. merged = append(merged, t1[i])
  35. i++
  36. } else {
  37. merged = append(merged, t2[j])
  38. j++
  39. }
  40. }
  41. merged = append(merged, t1[i:]...)
  42. merged = append(merged, t2[j:]...)
  43. if len(merged) == 0 {
  44. return merged
  45. }
  46. result := merged[:1]
  47. for _, iv := range merged[1:] {
  48. last := &result[len(result)-1]
  49. if iv.first <= last.last+1 {
  50. if iv.last > last.last {
  51. last.last = iv.last
  52. }
  53. } else {
  54. result = append(result, iv)
  55. }
  56. }
  57. return result
  58. }
  59. func handleEnv() {
  60. env := os.Getenv("RUNEWIDTH_EASTASIAN")
  61. if env == "" {
  62. EastAsianWidth = IsEastAsian()
  63. } else {
  64. EastAsianWidth = env == "1"
  65. }
  66. // update DefaultCondition
  67. if DefaultCondition.EastAsianWidth != EastAsianWidth {
  68. DefaultCondition.EastAsianWidth = EastAsianWidth
  69. if len(DefaultCondition.combinedLut) > 0 {
  70. DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
  71. CreateLUT()
  72. }
  73. }
  74. }
  75. type interval struct {
  76. first rune
  77. last rune
  78. }
  79. type table []interval
  80. func inTable(r rune, t table) bool {
  81. if r < t[0].first {
  82. return false
  83. }
  84. if r > t[len(t)-1].last {
  85. return false
  86. }
  87. bot := 0
  88. top := len(t) - 1
  89. for top >= bot {
  90. mid := (bot + top) >> 1
  91. switch {
  92. case t[mid].last < r:
  93. bot = mid + 1
  94. case t[mid].first > r:
  95. top = mid - 1
  96. default:
  97. return true
  98. }
  99. }
  100. return false
  101. }
  102. var private = table{
  103. {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
  104. }
  105. var nonprint = table{
  106. {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
  107. {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
  108. {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
  109. {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
  110. }
  111. // Condition have flag EastAsianWidth whether the current locale is CJK or not.
  112. type Condition struct {
  113. combinedLut []byte
  114. EastAsianWidth bool
  115. StrictEmojiNeutral bool
  116. }
  117. // NewCondition return new instance of Condition which is current locale.
  118. func NewCondition() *Condition {
  119. return &Condition{
  120. EastAsianWidth: EastAsianWidth,
  121. StrictEmojiNeutral: StrictEmojiNeutral,
  122. }
  123. }
  124. // RuneWidth returns the number of cells in r.
  125. // See http://www.unicode.org/reports/tr11/
  126. func (c *Condition) RuneWidth(r rune) int {
  127. if r < 0 || r > 0x10FFFF {
  128. return 0
  129. }
  130. if len(c.combinedLut) > 0 {
  131. return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
  132. }
  133. // optimized version, verified by TestRuneWidthChecksums()
  134. if !c.EastAsianWidth {
  135. switch {
  136. case r < 0x20:
  137. return 0
  138. case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
  139. return 0
  140. case r < 0x300:
  141. return 1
  142. case inTable(r, zerowidth):
  143. return 0
  144. case inTable(r, doublewidth):
  145. return 2
  146. default:
  147. return 1
  148. }
  149. } else {
  150. switch {
  151. case inTable(r, zerowidth):
  152. return 0
  153. case inTable(r, narrow):
  154. return 1
  155. case inTable(r, widewidth):
  156. return 2
  157. case !c.StrictEmojiNeutral && inTable(r, emoji):
  158. return 2
  159. default:
  160. return 1
  161. }
  162. }
  163. }
  164. // CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
  165. // This should not be called concurrently with other operations on c.
  166. // If options in c is changed, CreateLUT should be called again.
  167. func (c *Condition) CreateLUT() {
  168. const max = 0x110000
  169. lut := c.combinedLut
  170. if len(c.combinedLut) != 0 {
  171. // Remove so we don't use it.
  172. c.combinedLut = nil
  173. } else {
  174. lut = make([]byte, max/2)
  175. }
  176. for i := range lut {
  177. i32 := int32(i * 2)
  178. x0 := c.RuneWidth(i32)
  179. x1 := c.RuneWidth(i32 + 1)
  180. lut[i] = uint8(x0) | uint8(x1)<<4
  181. }
  182. c.combinedLut = lut
  183. }
  184. // StringWidth return width as you can see
  185. func (c *Condition) StringWidth(s string) (width int) {
  186. if len(s) > 0 && len(s) <= utf8.UTFMax {
  187. r, size := utf8.DecodeRuneInString(s)
  188. if size == len(s) {
  189. return c.RuneWidth(r)
  190. }
  191. }
  192. // ASCII fast path: no grapheme clustering needed for pure ASCII
  193. if isAllASCII(s) {
  194. for i := 0; i < len(s); i++ {
  195. b := s[i]
  196. if b >= 0x20 && b != 0x7F {
  197. width++
  198. }
  199. }
  200. return
  201. }
  202. g := graphemes.FromString(s)
  203. for g.Next() {
  204. var chWidth int
  205. for _, r := range g.Value() {
  206. chWidth = c.RuneWidth(r)
  207. if chWidth > 0 {
  208. break // Our best guess at this point is to use the width of the first non-zero-width rune.
  209. }
  210. }
  211. width += chWidth
  212. }
  213. return
  214. }
  215. func isAllASCII(s string) bool {
  216. for i := 0; i < len(s); i++ {
  217. if s[i] >= 0x80 {
  218. return false
  219. }
  220. }
  221. return true
  222. }
  223. // Truncate return string truncated with w cells
  224. func (c *Condition) Truncate(s string, w int, tail string) string {
  225. if c.StringWidth(s) <= w {
  226. return s
  227. }
  228. w -= c.StringWidth(tail)
  229. var width int
  230. pos := len(s)
  231. g := graphemes.FromString(s)
  232. for g.Next() {
  233. var chWidth int
  234. for _, r := range g.Value() {
  235. chWidth = c.RuneWidth(r)
  236. if chWidth > 0 {
  237. break // See StringWidth() for details.
  238. }
  239. }
  240. if width+chWidth > w {
  241. pos = g.Start()
  242. break
  243. }
  244. width += chWidth
  245. }
  246. return s[:pos] + tail
  247. }
  248. // TruncateLeft cuts w cells from the beginning of the `s`.
  249. func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
  250. if c.StringWidth(s) <= w {
  251. return prefix
  252. }
  253. var width int
  254. pos := len(s)
  255. g := graphemes.FromString(s)
  256. for g.Next() {
  257. var chWidth int
  258. for _, r := range g.Value() {
  259. chWidth = c.RuneWidth(r)
  260. if chWidth > 0 {
  261. break // See StringWidth() for details.
  262. }
  263. }
  264. if width+chWidth > w {
  265. if width < w {
  266. pos = g.End()
  267. prefix += strings.Repeat(" ", width+chWidth-w)
  268. } else {
  269. pos = g.Start()
  270. }
  271. break
  272. }
  273. width += chWidth
  274. }
  275. return prefix + s[pos:]
  276. }
  277. // Wrap return string wrapped with w cells
  278. func (c *Condition) Wrap(s string, w int) string {
  279. width := 0
  280. var out strings.Builder
  281. out.Grow(len(s) + len(s)/w + 1)
  282. for _, r := range s {
  283. cw := c.RuneWidth(r)
  284. if r == '\n' {
  285. out.WriteRune(r)
  286. width = 0
  287. continue
  288. } else if width+cw > w {
  289. out.WriteByte('\n')
  290. width = 0
  291. out.WriteRune(r)
  292. width += cw
  293. continue
  294. }
  295. out.WriteRune(r)
  296. width += cw
  297. }
  298. return out.String()
  299. }
  300. // FillLeft return string filled in left by spaces in w cells
  301. func (c *Condition) FillLeft(s string, w int) string {
  302. width := c.StringWidth(s)
  303. count := w - width
  304. if count > 0 {
  305. b := make([]byte, count)
  306. for i := range b {
  307. b[i] = ' '
  308. }
  309. return string(b) + s
  310. }
  311. return s
  312. }
  313. // FillRight return string filled in left by spaces in w cells
  314. func (c *Condition) FillRight(s string, w int) string {
  315. width := c.StringWidth(s)
  316. count := w - width
  317. if count > 0 {
  318. b := make([]byte, count)
  319. for i := range b {
  320. b[i] = ' '
  321. }
  322. return s + string(b)
  323. }
  324. return s
  325. }
  326. // RuneWidth returns the number of cells in r.
  327. // See http://www.unicode.org/reports/tr11/
  328. func RuneWidth(r rune) int {
  329. return DefaultCondition.RuneWidth(r)
  330. }
  331. // IsAmbiguousWidth returns whether is ambiguous width or not.
  332. func IsAmbiguousWidth(r rune) bool {
  333. return inTable(r, private) || inTable(r, ambiguous)
  334. }
  335. // IsCombiningWidth returns whether is combining width or not.
  336. func IsCombiningWidth(r rune) bool {
  337. return inTable(r, combining)
  338. }
  339. // IsNeutralWidth returns whether is neutral width or not.
  340. func IsNeutralWidth(r rune) bool {
  341. return inTable(r, neutral)
  342. }
  343. // StringWidth return width as you can see
  344. func StringWidth(s string) (width int) {
  345. return DefaultCondition.StringWidth(s)
  346. }
  347. // Truncate return string truncated with w cells
  348. func Truncate(s string, w int, tail string) string {
  349. return DefaultCondition.Truncate(s, w, tail)
  350. }
  351. // TruncateLeft cuts w cells from the beginning of the `s`.
  352. func TruncateLeft(s string, w int, prefix string) string {
  353. return DefaultCondition.TruncateLeft(s, w, prefix)
  354. }
  355. // Wrap return string wrapped with w cells
  356. func Wrap(s string, w int) string {
  357. return DefaultCondition.Wrap(s, w)
  358. }
  359. // FillLeft return string filled in left by spaces in w cells
  360. func FillLeft(s string, w int) string {
  361. return DefaultCondition.FillLeft(s, w)
  362. }
  363. // FillRight return string filled in left by spaces in w cells
  364. func FillRight(s string, w int) string {
  365. return DefaultCondition.FillRight(s, w)
  366. }
  367. // CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
  368. // This should not be called concurrently with other operations.
  369. func CreateLUT() {
  370. if len(DefaultCondition.combinedLut) > 0 {
  371. return
  372. }
  373. DefaultCondition.CreateLUT()
  374. }