package telegram import ( "html" "regexp" "strings" ) // FormatTelegramHTML converts standard Markdown to the HTML subset supported by // Telegram's Bot API. Unsupported constructs (tables, images, etc.) are // gracefully degraded to plain text. // // Supported Telegram HTML tags: , , , , ,
, , 
func FormatTelegramHTML(md string) string { md = strings.ReplaceAll(md, "\r\n", "\n") var out strings.Builder lines := strings.Split(md, "\n") inCodeBlock := false var codeLang string var codeLines []string inTable := false var tableRows [][]string for i := 0; i < len(lines); i++ { line := lines[i] if strings.HasPrefix(line, "```") { if !inCodeBlock { inCodeBlock = true codeLang = strings.TrimSpace(strings.TrimPrefix(line, "```")) codeLines = nil } else { inCodeBlock = false if codeLang != "" { out.WriteString("
")
				} else {
					out.WriteString("
")
				}
				out.WriteString(html.EscapeString(strings.Join(codeLines, "\n")))
				if codeLang != "" {
					out.WriteString("
\n") } else { out.WriteString("
\n") } } continue } if inCodeBlock { codeLines = append(codeLines, line) continue } if isTableRow(line) { if !inTable { inTable = true tableRows = nil } if isTableSeparator(line) { continue } tableRows = append(tableRows, parseTableRow(line)) continue } if inTable { flushTable(&out, tableRows) inTable = false tableRows = nil } if line == "---" || line == "***" || line == "___" { out.WriteString("——————\n") continue } if m := reHeading.FindStringSubmatch(line); m != nil { out.WriteString("" + formatInline(html.EscapeString(m[2])) + "\n") continue } if m := reBlockquote.FindStringSubmatch(line); m != nil { out.WriteString("
" + formatInline(html.EscapeString(m[1])) + "
\n") continue } if m := reUnorderedList.FindStringSubmatch(line); m != nil { out.WriteString("• " + formatInline(html.EscapeString(m[1])) + "\n") continue } if m := reOrderedList.FindStringSubmatch(line); m != nil { out.WriteString(m[1] + ". " + formatInline(html.EscapeString(m[2])) + "\n") continue } out.WriteString(formatInline(html.EscapeString(line)) + "\n") } if inCodeBlock && len(codeLines) > 0 { out.WriteString("
" + html.EscapeString(strings.Join(codeLines, "\n")) + "
\n") } if inTable { flushTable(&out, tableRows) } return strings.TrimRight(out.String(), "\n") } var ( reHeading = regexp.MustCompile(`^(#{1,6})\s+(.+)$`) reBlockquote = regexp.MustCompile(`^>\s*(.*)$`) reUnorderedList = regexp.MustCompile(`^[\s]*[-*+]\s+(.+)$`) reOrderedList = regexp.MustCompile(`^[\s]*(\d+)[.)]\s+(.+)$`) reBold = regexp.MustCompile(`\*\*(.+?)\*\*`) reBoldAlt = regexp.MustCompile(`__(.+?)__`) reItalic = regexp.MustCompile(`(?:^|[^*])\*([^*]+?)\*(?:[^*]|$)`) reItalicAlt = regexp.MustCompile(`(?:^|[^_])_([^_]+?)_(?:[^_]|$)`) reStrikethrough = regexp.MustCompile(`~~(.+?)~~`) reCode = regexp.MustCompile("`([^`]+)`") reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) reTableRow = regexp.MustCompile(`^\|.*\|$`) reTableSep = regexp.MustCompile(`^\|[\s\-:|]+\|$`) ) // formatInline applies inline Markdown formatting to already HTML-escaped text. // Order matters: code first (to protect its content), then links, bold, italic, etc. func formatInline(escaped string) string { escaped = reCode.ReplaceAllString(escaped, "$1") escaped = reLink.ReplaceAllStringFunc(escaped, func(match string) string { m := reLink.FindStringSubmatch(match) if len(m) < 3 { return match } return `
` + m[1] + `` }) escaped = reBold.ReplaceAllString(escaped, "$1") escaped = reBoldAlt.ReplaceAllString(escaped, "$1") escaped = reStrikethrough.ReplaceAllString(escaped, "$1") return escaped } func unescapeHTML(s string) string { return html.UnescapeString(s) } func isTableRow(line string) bool { return reTableRow.MatchString(strings.TrimSpace(line)) } func isTableSeparator(line string) bool { return reTableSep.MatchString(strings.TrimSpace(line)) } func parseTableRow(line string) []string { line = strings.TrimSpace(line) line = strings.TrimPrefix(line, "|") line = strings.TrimSuffix(line, "|") cells := strings.Split(line, "|") for i := range cells { cells[i] = strings.TrimSpace(cells[i]) } return cells } func flushTable(out *strings.Builder, rows [][]string) { if len(rows) == 0 { return } out.WriteString("
")
	colWidths := make([]int, len(rows[0]))
	for _, row := range rows {
		for i, cell := range row {
			if i < len(colWidths) && len(cell) > colWidths[i] {
				colWidths[i] = len(cell)
			}
		}
	}
	for ri, row := range rows {
		for ci, cell := range row {
			if ci > 0 {
				out.WriteString(" | ")
			}
			w := 0
			if ci < len(colWidths) {
				w = colWidths[ci]
			}
			out.WriteString(html.EscapeString(padRight(cell, w)))
		}
		out.WriteString("\n")
		if ri == 0 && len(rows) > 1 {
			for ci := range row {
				if ci > 0 {
					out.WriteString("-+-")
				}
				w := 0
				if ci < len(colWidths) {
					w = colWidths[ci]
				}
				out.WriteString(strings.Repeat("-", w))
			}
			out.WriteString("\n")
		}
	}
	out.WriteString("
\n") } func padRight(s string, width int) string { if len(s) >= width { return s } return s + strings.Repeat(" ", width-len(s)) }