Files
2026-05-27 09:55:11 +09:00

175 lines
4.4 KiB
Go

package main
import (
"fmt"
"html"
"sort"
"strings"
"unicode/utf16"
maxschemes "github.com/max-messenger/max-bot-api-client-go/schemes"
)
// --- TG Entities → Markdown (для MAX) ---
// tgEntitiesToMarkdown конвертирует TG text + entities в markdown-текст для MAX.
// Обрабатывает edge cases: пробелы перед/после маркеров выносятся за пределы тегов.
func tgEntitiesToMarkdown(text string, entities []Entity) string {
if len(entities) == 0 {
return text
}
// Конвертируем в UTF-16 для корректных offsets (TG использует UTF-16)
runes := []rune(text)
utf16units := utf16.Encode(runes)
// Собираем фрагменты: чередуя plain text и форматированные куски
// Работаем в UTF-16 координатах
type fragment struct {
start, end int // UTF-16 offsets
entity *Entity
}
// Сортируем entities по offset
sorted := make([]Entity, len(entities))
copy(sorted, entities)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Offset < sorted[j].Offset
})
var sb strings.Builder
pos := 0
for i := range sorted {
e := &sorted[i]
var open, close string
switch e.Type {
case "bold":
open, close = "**", "**"
case "italic":
open, close = "_", "_"
case "code":
open, close = "`", "`"
case "pre":
open, close = "```\n", "\n```"
case "strikethrough":
open, close = "~~", "~~"
case "text_link":
open = "["
close = fmt.Sprintf("](%s)", e.URL)
default:
continue
}
// Текст до entity
if e.Offset > pos {
sb.WriteString(utf16ToString(utf16units[pos:e.Offset]))
}
// Текст entity
end := e.Offset + e.Length
if end > len(utf16units) {
end = len(utf16units)
}
inner := utf16ToString(utf16units[e.Offset:end])
// Trim пробелов: выносим leading/trailing пробелы за маркеры
trimmed := strings.TrimRight(inner, " \t\n")
trailingSpaces := inner[len(trimmed):]
trimmed2 := strings.TrimLeft(trimmed, " \t\n")
leadingSpaces := trimmed[:len(trimmed)-len(trimmed2)]
sb.WriteString(leadingSpaces)
if trimmed2 != "" {
sb.WriteString(open)
sb.WriteString(trimmed2)
sb.WriteString(close)
}
sb.WriteString(trailingSpaces)
pos = end
}
// Остаток текста
if pos < len(utf16units) {
sb.WriteString(utf16ToString(utf16units[pos:]))
}
return sb.String()
}
// utf16ToString конвертирует UTF-16 slice обратно в Go string.
func utf16ToString(units []uint16) string {
runes := utf16.Decode(units)
return string(runes)
}
// --- MAX Markups → TG HTML ---
// maxMarkupsToHTML конвертирует MAX text + markups в TG-совместимый HTML.
func maxMarkupsToHTML(text string, markups []maxschemes.MarkUp) string {
if len(markups) == 0 {
return html.EscapeString(text)
}
runes := []rune(text)
utf16units := utf16.Encode(runes)
type tag struct {
pos int
open bool
order int
tag string
}
var tags []tag
for _, m := range markups {
var openTag, closeTag string
switch m.Type {
case maxschemes.MarkupStrong:
openTag, closeTag = "<b>", "</b>"
case maxschemes.MarkupEmphasized:
openTag, closeTag = "<i>", "</i>"
case maxschemes.MarkupMonospaced:
openTag, closeTag = "<code>", "</code>"
case maxschemes.MarkupStrikethrough:
openTag, closeTag = "<s>", "</s>"
case maxschemes.MarkupUnderline:
openTag, closeTag = "<u>", "</u>"
case maxschemes.MarkupLink:
openTag = `<a href="` + html.EscapeString(m.URL) + `">`
closeTag = "</a>"
default:
continue
}
tags = append(tags, tag{pos: m.From, open: true, order: 0, tag: openTag})
tags = append(tags, tag{pos: m.From + m.Length, open: false, order: 1, tag: closeTag})
}
sort.Slice(tags, func(i, j int) bool {
if tags[i].pos != tags[j].pos {
return tags[i].pos < tags[j].pos
}
return tags[i].order > tags[j].order
})
var sb strings.Builder
tagIdx := 0
for i := 0; i <= len(utf16units); i++ {
for tagIdx < len(tags) && tags[tagIdx].pos == i {
sb.WriteString(tags[tagIdx].tag)
tagIdx++
}
if i < len(utf16units) {
if utf16.IsSurrogate(rune(utf16units[i])) && i+1 < len(utf16units) {
r := utf16.DecodeRune(rune(utf16units[i]), rune(utf16units[i+1]))
sb.WriteString(html.EscapeString(string(r)))
i++
} else {
sb.WriteString(html.EscapeString(string(rune(utf16units[i]))))
}
}
}
return sb.String()
}