135 lines
2.8 KiB
Go
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("&", "&", "<", "<", ">", ">")
|
|
|
|
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()
|
|
}
|