package services import ( "fmt" "regexp" "strings" ) var ( reCodeBlock = regexp.MustCompile("(?s)```(?:[a-zA-Z0-9]*\n)?(.*?)```") reInlineCode = regexp.MustCompile("`([^`\n]+)`") reBold = regexp.MustCompile(`\*\*(.+?)\*\*|__(.+?)__`) reHeader = regexp.MustCompile(`(?m)^#{1,6}[ \t]+(.+)$`) reBullet = regexp.MustCompile(`(?m)^(\s*)[-*][ \t]+`) reHRSimple = regexp.MustCompile(`(?m)^[ \t]*(-{3,}|_{3,}|\*{3,})[ \t]*\n?`) reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)`) ) // FormatearParaTelegram convierte el Markdown que suele escribir la IA (##, // **negrita**, listas con "-", ``` código ```, separadores ---) a las pocas // etiquetas HTML que Telegram sí entiende con parse_mode=HTML (b, i, code, pre, // a). Escapa primero cualquier < > & literal del texto original para que // sendMessage nunca falle por HTML inválido si la IA menciona código o símbolos. func FormatearParaTelegram(text string) string { // 1. Proteger bloques de código ANTES de escapar el resto, para no // procesarles negrita/viñetas por accidente. var blocks []string protect := func(html string) string { blocks = append(blocks, html) return fmt.Sprintf("\x00%d\x00", len(blocks)-1) } text = reCodeBlock.ReplaceAllStringFunc(text, func(m string) string { sub := reCodeBlock.FindStringSubmatch(m) return protect("
" + escapeTelegramHTML(strings.TrimSpace(sub[1])) + "
") }) text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string { sub := reInlineCode.FindStringSubmatch(m) return protect("" + escapeTelegramHTML(sub[1]) + "") }) // 2. Escapar el resto del texto como HTML. text = escapeTelegramHTML(text) // 3. Separadores horizontales estilo markdown (---, ___, ***) fuera. text = reHRSimple.ReplaceAllString(text, "") // 4. Negrita (antes que encabezados, para no anidar si un título // también trae **negrita** adentro). text = reBold.ReplaceAllStringFunc(text, func(m string) string { sub := reBold.FindStringSubmatch(m) inner := sub[1] if inner == "" { inner = sub[2] } return "" + inner + "" }) // 5. Encabezados (#, ##, ###...) → negrita, sin duplicar si ya la trae. text = reHeader.ReplaceAllStringFunc(text, func(m string) string { sub := reHeader.FindStringSubmatch(m) inner := strings.NewReplacer("", "", "", "").Replace(sub[1]) return "" + inner + "" }) // 6. Viñetas "- " o "* " al inicio de línea → "• ". text = reBullet.ReplaceAllString(text, "$1• ") // 7. Enlaces [texto](url). text = reLink.ReplaceAllString(text, `$1`) // 8. Restaurar bloques de código protegidos en el paso 1. for i, b := range blocks { text = strings.ReplaceAll(text, fmt.Sprintf("\x00%d\x00", i), b) } // Máximo 3 saltos de línea seguidos (Markdown suele dejar párrafos sueltos). for strings.Contains(text, "\n\n\n\n") { text = strings.ReplaceAll(text, "\n\n\n\n", "\n\n\n") } return strings.TrimSpace(text) }