package shared import ( "strings" "unicode/utf8" "github.com/mymmrac/telego" tu "github.com/mymmrac/telego/telegoutil" ) var escaper = strings.NewReplacer("&", "&", "<", "<", ">", ">") 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("') } return closers.String() }