package telegram import ( "html" "sort" "strconv" "strings" "unicode/utf8" "github.com/gotd/td/tg" ) type htmlEntity struct { start int end int openTag string closeTag string length int } type htmlEvent struct { pos int tag string isStart bool length int } func messageToHTML(text string, entities []tg.MessageEntityClass) string { if text == "" { return "" } if len(entities) == 0 { return html.EscapeString(text) } ranges := make([]htmlEntity, 0, len(entities)) needed := make([]int, 0, len(entities)*2) for _, raw := range entities { offset, length, openTag, closeTag, ok := htmlEntityMeta(raw) if !ok { continue } start := offset end := offset + length if length <= 0 { continue } ranges = append(ranges, htmlEntity{ start: start, end: end, openTag: openTag, closeTag: closeTag, length: length, }) needed = append(needed, start, end) } if len(ranges) == 0 { return html.EscapeString(text) } positions := utf16PositionsToBytes(text, needed) events := make([]htmlEvent, 0, len(ranges)*2) for _, r := range ranges { startByte, okStart := positions[r.start] endByte, okEnd := positions[r.end] if !okStart || !okEnd || startByte > endByte { continue } events = append(events, htmlEvent{ pos: startByte, tag: r.openTag, isStart: true, length: r.length, }) events = append(events, htmlEvent{ pos: endByte, tag: r.closeTag, isStart: false, length: r.length, }) } sort.SliceStable(events, func(i, j int) bool { if events[i].pos != events[j].pos { return events[i].pos < events[j].pos } if events[i].isStart != events[j].isStart { return !events[i].isStart } if events[i].isStart { return events[i].length > events[j].length } return events[i].length < events[j].length }) var b strings.Builder last := 0 for _, ev := range events { if ev.pos > last { b.WriteString(html.EscapeString(text[last:ev.pos])) } b.WriteString(ev.tag) last = ev.pos } if last < len(text) { b.WriteString(html.EscapeString(text[last:])) } return b.String() } func htmlEntityMeta(entity tg.MessageEntityClass) (offset int, length int, openTag string, closeTag string, ok bool) { switch e := entity.(type) { case *tg.MessageEntityBold: return e.Offset, e.Length, "", "", true case *tg.MessageEntityItalic: return e.Offset, e.Length, "", "", true case *tg.MessageEntityUnderline: return e.Offset, e.Length, "", "", true case *tg.MessageEntityStrike: return e.Offset, e.Length, "", "", true case *tg.MessageEntityCode: return e.Offset, e.Length, "", "", true case *tg.MessageEntityPre: if e.Language != "" { lang := html.EscapeString(e.Language) return e.Offset, e.Length, `
`, "
", true } return e.Offset, e.Length, "
", "
", true case *tg.MessageEntityTextURL: url := html.EscapeString(e.URL) return e.Offset, e.Length, ``, "", true case *tg.MessageEntityMentionName: userID := html.EscapeString(strconv.FormatInt(e.UserID, 10)) return e.Offset, e.Length, ``, "", true case *tg.MessageEntitySpoiler: return e.Offset, e.Length, ``, "", true case *tg.MessageEntityBlockquote: if e.Collapsed { return e.Offset, e.Length, `
`, "
", true } return e.Offset, e.Length, "
", "
", true case *tg.MessageEntityCustomEmoji: id := html.EscapeString(strconv.FormatInt(e.DocumentID, 10)) return e.Offset, e.Length, ``, "", true default: return 0, 0, "", "", false } } func utf16PositionsToBytes(text string, needed []int) map[int]int { result := make(map[int]int, len(needed)) needSet := make(map[int]struct{}, len(needed)) for _, n := range needed { needSet[n] = struct{}{} } utf16Pos := 0 if _, ok := needSet[0]; ok { result[0] = 0 } for i, r := range text { if _, ok := needSet[utf16Pos]; ok { result[utf16Pos] = i } step := utf16RuneLen(r) if step == 2 { if _, ok := needSet[utf16Pos+1]; ok { result[utf16Pos+1] = i } } utf16Pos += step } if _, ok := needSet[utf16Pos]; ok { result[utf16Pos] = len(text) } return result } func utf16RuneLen(r rune) int { const surrSelf = 0x10000 if r >= surrSelf && r <= utf8.MaxRune { return 2 } return 1 }