gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package util
  4. import (
  5. "strings"
  6. "unicode"
  7. "unicode/utf8"
  8. )
  9. // in UTF8 "…" is 3 bytes so doesn't really gain us anything...
  10. const (
  11. utf8Ellipsis = "…"
  12. asciiEllipsis = "..."
  13. )
  14. func IsLikelyEllipsisLeftPart(s string) bool {
  15. return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
  16. }
  17. func ellipsisDisplayGuessWidth(r rune) int {
  18. // To make the truncated string as long as possible,
  19. // CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
  20. // Here we only make the best guess (better than counting them in bytes),
  21. // it's impossible to 100% correctly determine the width of a rune without a real font and render.
  22. //
  23. // ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment
  24. if r <= 255 {
  25. return 1
  26. }
  27. switch {
  28. case r == '\u3000': /* ideographic (CJK) characters, still use 2 */
  29. return 2
  30. case unicode.Is(unicode.M, r), /* (Mark) */
  31. unicode.Is(unicode.Cf, r), /* (Other, format) */
  32. unicode.Is(unicode.Cs, r), /* (Other, surrogate) */
  33. unicode.Is(unicode.Z /* (Space) */, r):
  34. return 1
  35. default:
  36. return 2
  37. }
  38. }
  39. // EllipsisDisplayString returns a truncated short string for display purpose.
  40. // The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width)
  41. // It appends "…" or "..." at the end of truncated string.
  42. // It guarantees the length of the returned runes doesn't exceed the limit.
  43. func EllipsisDisplayString(str string, limit int) string {
  44. s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
  45. return s
  46. }
  47. // EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
  48. func EllipsisDisplayStringX(str string, limit int) (left, right string) {
  49. return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
  50. }
  51. func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
  52. left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
  53. if truncated {
  54. right = str[offset:]
  55. r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
  56. encounterInvalid = encounterInvalid || r == utf8.RuneError
  57. ellipsis := utf8Ellipsis
  58. if encounterInvalid {
  59. ellipsis = asciiEllipsis
  60. }
  61. right = ellipsis + right
  62. }
  63. return left, right
  64. }
  65. func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
  66. if len(str) <= limit {
  67. return str, len(str), false, false
  68. }
  69. // To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit,
  70. // because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters,
  71. // So each rune must be countered as at least 1 width.
  72. // Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero.
  73. pos, used := 0, 0
  74. for i, r := range str {
  75. encounterInvalid = encounterInvalid || r == utf8.RuneError
  76. pos = i
  77. runeWidth := widthGuess(r)
  78. if used+runeWidth+3 > limit {
  79. break
  80. }
  81. used += runeWidth
  82. offset += utf8.RuneLen(r)
  83. }
  84. // if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse
  85. if len(str)-pos <= 12 {
  86. var nextCnt, nextWidth int
  87. for _, r := range str[pos:] {
  88. if nextCnt >= 4 {
  89. break
  90. }
  91. nextWidth += widthGuess(r)
  92. nextCnt++
  93. }
  94. if nextCnt <= 3 && used+nextWidth <= limit {
  95. return str, len(str), false, false
  96. }
  97. }
  98. if limit < 3 {
  99. // if the limit is so small, do not add ellipsis
  100. return str[:offset], offset, true, false
  101. }
  102. ellipsis := utf8Ellipsis
  103. if encounterInvalid {
  104. ellipsis = asciiEllipsis
  105. }
  106. return str[:offset] + ellipsis, offset, true, encounterInvalid
  107. }
  108. func EllipsisTruncateRunes(str string, limit int) (left, right string) {
  109. return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
  110. }
  111. // TruncateRunes returns a truncated string with given rune limit,
  112. // it returns input string if its rune length doesn't exceed the limit.
  113. func TruncateRunes(str string, limit int) string {
  114. if utf8.RuneCountInString(str) < limit {
  115. return str
  116. }
  117. return string([]rune(str)[:limit])
  118. }