picoclaw/pkg/channels/telegram/parse_markdown_to_md_v2.go
Aleksandr Bortnikov 83a831e193 fix: case with ~
2026-03-10 18:38:08 +03:00

163 lines
4.3 KiB
Go

package telegram
import (
"strings"
)
// markdownToTelegramMarkdownV2 takes a standardized markdown string and
// strictly escapes or transforms it to fit Telegram's MarkdownV2 requirements.
// https://core.telegram.org/bots/api#formatting-options
func markdownToTelegramMarkdownV2(text string) string {
// replace Heading to bolding
text = reHeading.ReplaceAllString(text, "*$1*")
text = reBoldStar.ReplaceAllString(text, "*$1*")
var result strings.Builder
runes := []rune(text)
length := len(runes)
// List of characters that must be escaped in standard text contexts
needsNormalEscape := func(r rune) bool {
switch r {
case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!':
return true
}
return false
}
i := 0
for i < length {
// 1. Check for Pre-formatted Code Block (```...```)
if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
result.WriteString("```")
i += 3
// Find closing ```
for i < length {
if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
result.WriteString("```")
i += 3
break
}
// Inside code blocks, escape `\` and `\`
if runes[i] == '\\' || runes[i] == '`' {
result.WriteRune('\\')
}
result.WriteRune(runes[i])
i++
}
continue
}
// 2. Check for Inline Code (`...`)
if runes[i] == '`' {
result.WriteRune('`')
i++
for i < length {
if runes[i] == '`' {
result.WriteRune('`')
i++
break
}
if runes[i] == '\\' || runes[i] == '`' {
result.WriteRune('\\')
}
result.WriteRune(runes[i])
i++
}
continue
}
// 3. Link or Custom Emoji definition: URL part (...)
// We detect this by checking if the previous non-space character closed a bracket ']',
// and we are currently on '('. To keep logic linear, we handle it as we traverse.
// NOTE: A true deep-parser would link `[` to `](...)`. For safety, whenever we see `(`,
// if it looks like a URL part, we escape it via URL rules. Let's do a basic lookbehind.
if i != 0 && runes[i] == '(' && i > 0 && runes[i-1] == ']' {
result.WriteRune('(')
i++
for i < length {
if runes[i] == ')' {
// Unescaped closing bracket ends the URL
result.WriteRune(')')
i++
break
}
// In URL part, escape `\` and `)`
if runes[i] == '\\' || runes[i] == ')' {
result.WriteRune('\\')
}
result.WriteRune(runes[i])
i++
}
continue
}
// 4. Handle blockquotes starts
if i != 0 && runes[i] == '>' && (i == 0 || runes[i-1] == '\n') {
result.WriteRune('>')
i++
continue
}
// 5. Handle expandable block quotation starts
if i+3 < length && runes[i] == '>' && runes[i-1] == '*' && runes[i-2] == '*' && (i == 0 || runes[i-3] == '\n') {
result.WriteRune(runes[i])
i++
continue
}
// 6. Handle standard Markdown Entities Boundaries
// If they are part of valid markdown boundaries, we write them as-is.
// We trust the syntax rules: * _ ~ || [ ]
// (Assuming the text is a valid markdown, we don't escape these if formatting is intended)
// Note on Ambiguity (__ vs _):
// Telegram parses `__` from left to right greedily.
if i+1 < length && runes[i] == '_' && runes[i+1] == '_' {
result.WriteString("__")
i += 2
continue
}
if i+1 < length && runes[i] == '|' && runes[i+1] == '|' {
result.WriteString("||")
i += 2
continue
}
// Standard single-char boundaries
if runes[i] == '*' || runes[i] == '_' || runes[i] == '[' || runes[i] == ']' {
result.WriteRune(runes[i])
i++
continue
}
// Custom emoji boundary check `![`
if i+1 < length && runes[i] == '!' && runes[i+1] == '[' {
result.WriteString("![")
i += 2
continue
}
// 7. Handle plain text characters
// Escape remaining special characters if they aren't forming intended valid markup
if needsNormalEscape(runes[i]) {
// Check if it's already escaped; if an escape character exists, consume it legitimately
if runes[i] == '\\' && i+1 < length && needsNormalEscape(runes[i+1]) {
// Keep the backslash and the escaped char as is, avoiding double escaping
result.WriteRune('\\')
result.WriteRune(runes[i+1])
i += 2
continue
}
// Auto-escape the character
result.WriteRune('\\')
}
result.WriteRune(runes[i])
i++
}
return result.String()
}