| 1 | package textutil |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "github.com/rivo/uniseg" |
| 7 | ) |
| 8 | |
| 9 | // FitGraphemeBytes returns the longest prefix that fits maxBytes without |
| 10 | // splitting a grapheme cluster. If a single cluster is larger than maxBytes, it |
| 11 | // returns that whole cluster so callers never emit malformed user-visible text. |
| 12 | func FitGraphemeBytes(text string, maxBytes int) string { |
| 13 | if maxBytes <= 0 { |
| 14 | return "" |
| 15 | } |
| 16 | end := 0 |
| 17 | used := 0 |
| 18 | graphemes := uniseg.NewGraphemes(text) |
| 19 | for graphemes.Next() { |
| 20 | size := len(graphemes.Str()) |
| 21 | if used > 0 && used+size > maxBytes { |
| 22 | break |
| 23 | } |
| 24 | end += size |
| 25 | used += size |
| 26 | if used >= maxBytes { |
| 27 | break |
| 28 | } |
| 29 | } |
| 30 | if end > 0 { |
| 31 | return text[:end] |
| 32 | } |
| 33 | graphemes = uniseg.NewGraphemes(text) |
| 34 | if !graphemes.Next() { |
| 35 | return "" |
| 36 | } |
| 37 | return graphemes.Str() |
| 38 | } |
| 39 | |
| 40 | // ClipGraphemes truncates s to at most max grapheme clusters, counting suffix |
| 41 | // inside the budget when suffix is used. |
| 42 | func ClipGraphemes(s string, max int, suffix string) string { |
| 43 | if max < 1 { |
| 44 | max = 1 |
| 45 | } |
| 46 | clusters := collectGraphemes(s, max+1) |
| 47 | if len(clusters) <= max && len(clusters) == countGraphemes(s) { |
| 48 | return s |
| 49 | } |
| 50 | suffixClusters := countGraphemes(suffix) |
| 51 | keep := max - suffixClusters |
| 52 | if keep < 1 { |
| 53 | keep = 1 |
| 54 | suffix = "" |
| 55 | } |
| 56 | if keep > len(clusters) { |
| 57 | keep = len(clusters) |
| 58 | } |
| 59 | return strings.Join(clusters[:keep], "") + suffix |
| 60 | } |
| 61 | |
| 62 | // TruncateGraphemes truncates s to at most max grapheme clusters, then appends |
| 63 | // suffix outside that budget. This preserves legacy preview behavior where the |
| 64 | // suffix is an extra truncation marker rather than part of the display width. |
| 65 | func TruncateGraphemes(s string, max int, suffix string) string { |
| 66 | if max < 0 { |
| 67 | max = 0 |
| 68 | } |
| 69 | clusters := collectGraphemes(s, max+1) |
| 70 | if len(clusters) <= max && len(clusters) == countGraphemes(s) { |
| 71 | return s |
| 72 | } |
| 73 | if max > len(clusters) { |
| 74 | max = len(clusters) |
| 75 | } |
| 76 | return strings.Join(clusters[:max], "") + suffix |
| 77 | } |
| 78 | |
| 79 | func collectGraphemes(s string, limit int) []string { |
| 80 | if limit < 1 { |
| 81 | return nil |
| 82 | } |
| 83 | clusters := make([]string, 0, limit) |
| 84 | graphemes := uniseg.NewGraphemes(s) |
| 85 | for graphemes.Next() { |
| 86 | clusters = append(clusters, graphemes.Str()) |
| 87 | if len(clusters) >= limit { |
| 88 | break |
| 89 | } |
| 90 | } |
| 91 | return clusters |
| 92 | } |
| 93 | |
| 94 | func countGraphemes(s string) int { |
| 95 | count := 0 |
| 96 | graphemes := uniseg.NewGraphemes(s) |
| 97 | for graphemes.Next() { |
| 98 | count++ |
| 99 | } |
| 100 | return count |
| 101 | } |
| 102 |