163 lines
4.3 KiB
Go
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()
|
|
}
|