mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package util
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// in UTF8 "…" is 3 bytes so doesn't really gain us anything...
|
|
const (
|
|
utf8Ellipsis = "…"
|
|
asciiEllipsis = "..."
|
|
)
|
|
|
|
func IsLikelyEllipsisLeftPart(s string) bool {
|
|
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
|
|
}
|
|
|
|
func ellipsisDisplayGuessWidth(r rune) int {
|
|
// To make the truncated string as long as possible,
|
|
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
|
|
// Here we only make the best guess (better than counting them in bytes),
|
|
// it's impossible to 100% correctly determine the width of a rune without a real font and render.
|
|
//
|
|
// ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment
|
|
if r <= 255 {
|
|
return 1
|
|
}
|
|
|
|
switch {
|
|
case r == '\u3000': /* ideographic (CJK) characters, still use 2 */
|
|
return 2
|
|
case unicode.Is(unicode.M, r), /* (Mark) */
|
|
unicode.Is(unicode.Cf, r), /* (Other, format) */
|
|
unicode.Is(unicode.Cs, r), /* (Other, surrogate) */
|
|
unicode.Is(unicode.Z /* (Space) */, r):
|
|
return 1
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
// EllipsisDisplayString returns a truncated short string for display purpose.
|
|
// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width)
|
|
// It appends "…" or "..." at the end of truncated string.
|
|
// It guarantees the length of the returned runes doesn't exceed the limit.
|
|
func EllipsisDisplayString(str string, limit int) string {
|
|
s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
|
|
return s
|
|
}
|
|
|
|
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
|
|
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
|
|
return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
|
|
}
|
|
|
|
func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
|
|
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
|
|
if truncated {
|
|
right = str[offset:]
|
|
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
|
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
|
ellipsis := utf8Ellipsis
|
|
if encounterInvalid {
|
|
ellipsis = asciiEllipsis
|
|
}
|
|
right = ellipsis + right
|
|
}
|
|
return left, right
|
|
}
|
|
|
|
func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
|
|
if len(str) <= limit {
|
|
return str, len(str), false, false
|
|
}
|
|
|
|
// To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit,
|
|
// because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters,
|
|
// So each rune must be countered as at least 1 width.
|
|
// Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero.
|
|
pos, used := 0, 0
|
|
for i, r := range str {
|
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
|
pos = i
|
|
runeWidth := widthGuess(r)
|
|
if used+runeWidth+3 > limit {
|
|
break
|
|
}
|
|
used += runeWidth
|
|
offset += utf8.RuneLen(r)
|
|
}
|
|
|
|
// if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse
|
|
if len(str)-pos <= 12 {
|
|
var nextCnt, nextWidth int
|
|
for _, r := range str[pos:] {
|
|
if nextCnt >= 4 {
|
|
break
|
|
}
|
|
nextWidth += widthGuess(r)
|
|
nextCnt++
|
|
}
|
|
if nextCnt <= 3 && used+nextWidth <= limit {
|
|
return str, len(str), false, false
|
|
}
|
|
}
|
|
if limit < 3 {
|
|
// if the limit is so small, do not add ellipsis
|
|
return str[:offset], offset, true, false
|
|
}
|
|
ellipsis := utf8Ellipsis
|
|
if encounterInvalid {
|
|
ellipsis = asciiEllipsis
|
|
}
|
|
return str[:offset] + ellipsis, offset, true, encounterInvalid
|
|
}
|
|
|
|
func EllipsisTruncateRunes(str string, limit int) (left, right string) {
|
|
return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
|
|
}
|
|
|
|
// TruncateRunes returns a truncated string with given rune limit,
|
|
// it returns input string if its rune length doesn't exceed the limit.
|
|
func TruncateRunes(str string, limit int) string {
|
|
if utf8.RuneCountInString(str) < limit {
|
|
return str
|
|
}
|
|
return string([]rune(str)[:limit])
|
|
}
|