Files
2026-11-08 03:46:11 +00:00

135 lines
2.8 KiB
Go

package shared
import (
"strings"
"unicode/utf8"
"github.com/mymmrac/telego"
tu "github.com/mymmrac/telego/telegoutil"
)
var escaper = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
func Button(text, data string) telego.InlineKeyboardButton {
return tu.InlineKeyboardButton(text).WithCallbackData(data)
}
func Escape(text string) string {
return escaper.Replace(text)
}
func Truncate(text string, max int) string {
if utf8.RuneCountInString(text) <= max {
return text
}
runes := []rune(text)
return string(runes[:max-1]) + "…"
}
// TelegramMessageLimit is Telegram's maximum text length in characters.
const TelegramMessageLimit = 4096
// FitMessage truncates rendered HTML to the Telegram message limit without
// cutting an entity or leaving a tag open.
func FitMessage(text string) string {
runes := []rune(text)
if len(runes) <= TelegramMessageLimit {
return text
}
limit := TelegramMessageLimit - 1 // reserve the ellipsis
cut := safeCut(runes, limit)
for cut > 0 {
closers := closingTags(runes[:cut])
if cut+utf8.RuneCountInString(closers) <= limit {
return string(runes[:cut]) + closers + "…"
}
cut = safeCut(runes, cut-1)
}
return "…"
}
// safeCut returns the largest index up to limit that is not inside an HTML tag
// or entity.
func safeCut(runes []rune, limit int) int {
if limit > len(runes) {
limit = len(runes)
}
inTag := false
entityStart := -1
for i := 0; i < limit; i++ {
char := runes[i]
switch {
case inTag:
if char == '>' {
inTag = false
}
case entityStart >= 0:
if char == ';' {
entityStart = -1
}
case char == '<':
inTag = true
case char == '&':
entityStart = i
}
}
if inTag {
for i := limit - 1; i >= 0; i-- {
if runes[i] == '<' {
return i
}
}
return 0
}
if entityStart >= 0 {
return entityStart
}
return limit
}
// closingTags returns the closing tags needed for any HTML left open by prefix.
func closingTags(prefix []rune) string {
var open []string
for i := 0; i < len(prefix); i++ {
if prefix[i] != '<' {
continue
}
end := i + 1
for end < len(prefix) && prefix[end] != '>' {
end++
}
if end >= len(prefix) {
break
}
raw := strings.TrimSpace(string(prefix[i+1 : end]))
i = end
if raw == "" {
continue
}
if strings.HasPrefix(raw, "/") {
name := strings.TrimSpace(raw[1:])
if len(open) > 0 && open[len(open)-1] == name {
open = open[:len(open)-1]
}
continue
}
name := raw
if space := strings.IndexAny(name, " \t\n"); space >= 0 {
name = name[:space]
}
if strings.HasSuffix(name, "/") || name == "br" {
continue
}
open = append(open, name)
}
var closers strings.Builder
for i := len(open) - 1; i >= 0; i-- {
closers.WriteString("</")
closers.WriteString(open[i])
closers.WriteByte('>')
}
return closers.String()
}