Files
mygoTgChanBot/internal/toc/renderer.go
zuimao_kaf df1fb034b3 TOC分页:解决内容超长导致MEDIA_CAPTION_TOO_LONG错误
当目录条目增多超过Telegram caption字符限制时,自动按字符上限分页渲染,
附带翻页inline keyboard(⏮首页/⬅️上一页/➡️下一页),支持photo和text两种模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 22:15:54 +08:00

141 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package toc
import (
"fmt"
"html"
"regexp"
"strings"
"unicode/utf8"
)
var htmlTagRegexp = regexp.MustCompile(`<[^>]*>`)
// htmlTextLength 计算剥离 HTML 标签后的字符数,匹配 Telegram 的计数方式
func htmlTextLength(s string) int {
stripped := htmlTagRegexp.ReplaceAllString(s, "")
return utf8.RuneCountInString(stripped)
}
// renderLines 将所有分类和条目渲染为独立的行
func (m *Manager) renderLines() (header string, lines []string, footer string, err error) {
categories, err := m.storage.ListCategories()
if err != nil {
return "", nil, "", err
}
entriesByCategory, err := m.storage.GetEntriesByCategory()
if err != nil {
return "", nil, "", err
}
header = "📚 <b>频道目录</b>\n━━━━━━━━━━━━━━━\n\n"
footer = "━━━━━━━━━━━━━━━\n"
if len(categories) == 0 {
lines = append(lines, "<i>暂无分类</i>\n")
return header, lines, footer, nil
}
for _, cat := range categories {
entries := entriesByCategory[cat.Name]
catLine := fmt.Sprintf("📁 <b>%s</b>", html.EscapeString(cat.Name))
if len(entries) > 0 {
catLine += fmt.Sprintf(" (%d)", len(entries))
}
catLine += "\n"
lines = append(lines, catLine)
if len(entries) == 0 {
lines = append(lines, " <i>暂无内容</i>\n")
} else {
for _, entry := range entries {
entryLine := fmt.Sprintf(" • <a href=\"%s\">%s</a>", html.EscapeString(entry.MessageLink()), html.EscapeString(entry.Title))
if entry.HasDirectLink() {
entryLine += fmt.Sprintf(" <a href=\"%s\">[直达]</a>", html.EscapeString(entry.DirectLink))
}
entryLine += "\n"
lines = append(lines, entryLine)
}
}
// 分类之间空行
lines = append(lines, "\n")
}
return header, lines, footer, nil
}
// RenderPage 渲染指定页的 TOC 内容,返回内容、总页数
func (m *Manager) RenderPage(page, charLimit int) (string, int, error) {
header, lines, footer, err := m.renderLines()
if err != nil {
return "", 0, err
}
// 页码行的最大长度预估(如 "📄 99 / 99\n"
pageIndicatorLen := 15
headerLen := htmlTextLength(header)
footerLen := htmlTextLength(footer)
fixedLen := headerLen + footerLen + pageIndicatorLen
// 将行分配到各页
var pages [][]string
var currentPage []string
currentLen := fixedLen
for _, line := range lines {
lineLen := htmlTextLength(line)
// 如果当前页加上这行会超限,且当前页已有内容,则开新页
if currentLen+lineLen > charLimit && len(currentPage) > 0 {
pages = append(pages, currentPage)
currentPage = nil
currentLen = fixedLen
}
currentPage = append(currentPage, line)
currentLen += lineLen
}
// 最后一页
if len(currentPage) > 0 {
pages = append(pages, currentPage)
}
totalPages := len(pages)
if totalPages == 0 {
totalPages = 1
pages = [][]string{{}}
}
// clamp page
if page < 1 {
page = 1
}
if page > totalPages {
page = totalPages
}
// 组装当前页内容
var sb strings.Builder
sb.WriteString(header)
for _, line := range pages[page-1] {
sb.WriteString(line)
}
sb.WriteString(footer)
if totalPages > 1 {
sb.WriteString(fmt.Sprintf("📄 %d / %d\n", page, totalPages))
}
return sb.String(), totalPages, nil
}
// Render 渲染完整 TOC兼容方法返回第 1 页,使用 photo caption 限制)
func (m *Manager) Render() (string, error) {
content, _, err := m.RenderPage(1, captionCharLimit)
return content, err
}