-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmarkdown.go
More file actions
143 lines (131 loc) · 4.13 KB
/
Copy pathmarkdown.go
File metadata and controls
143 lines (131 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package telegram
import (
"fmt"
"html"
"regexp"
"strings"
)
var (
reFenceOpen = regexp.MustCompile("^```(\\w*)")
reInlineCode = regexp.MustCompile("`([^`\n]+)`")
reBoldStar = regexp.MustCompile(`\*\*(.+?)\*\*`)
reBoldUnder = regexp.MustCompile(`__(.+?)__`)
reItalicStar = regexp.MustCompile(`\*([^*\n]+)\*`)
reItalicUnder = regexp.MustCompile(`_([^_\n]+)_`)
reStrike = regexp.MustCompile(`~~(.+?)~~`)
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)`)
reHeader = regexp.MustCompile(`^#{1,6}\s+(.+)$`)
reBulletItem = regexp.MustCompile(`^(\s*)[-*]\s+(.+)$`)
reNumItem = regexp.MustCompile(`^(\s*\d+\.)\s+(.+)$`)
reCheckbox = regexp.MustCompile(`^\[[ ]\]\s*`)
reCheckboxDone = regexp.MustCompile(`^\[[xX]\]\s*`)
rePlaceholder = regexp.MustCompile(`\x00(\d+)\x00`)
)
// markdownToTelegramHTML converts LLM Markdown output to Telegram-compatible HTML.
func markdownToTelegramHTML(src string) string {
var sb strings.Builder
lines := strings.Split(src, "\n")
i := 0
for i < len(lines) {
line := lines[i]
// Fenced code block
if m := reFenceOpen.FindStringSubmatch(line); m != nil {
lang := m[1]
i++
var codeLines []string
for i < len(lines) && lines[i] != "```" {
codeLines = append(codeLines, lines[i])
i++
}
if i < len(lines) {
i++ // skip closing ```
}
code := html.EscapeString(strings.Join(codeLines, "\n"))
if lang != "" {
sb.WriteString(fmt.Sprintf("<pre><code class=\"language-%s\">%s</code></pre>\n", html.EscapeString(lang), code))
} else {
sb.WriteString("<pre><code>" + code + "</code></pre>\n")
}
continue
}
sb.WriteString(processLine(line))
sb.WriteByte('\n')
i++
}
return strings.TrimSpace(sb.String())
}
func processLine(line string) string {
if m := reHeader.FindStringSubmatch(line); m != nil {
return "<b>" + processInline(m[1]) + "</b>"
}
if m := reBulletItem.FindStringSubmatch(line); m != nil {
indent := strings.Repeat(" ", len(m[1])/2)
rest := m[2]
prefix := "• "
if reCheckboxDone.MatchString(rest) {
rest = reCheckboxDone.ReplaceAllString(rest, "")
prefix = "☑ "
} else if reCheckbox.MatchString(rest) {
rest = reCheckbox.ReplaceAllString(rest, "")
prefix = "☐ "
}
return indent + prefix + processInline(rest)
}
if m := reNumItem.FindStringSubmatch(line); m != nil {
return html.EscapeString(m[1]) + " " + processInline(m[2])
}
return processInline(line)
}
func processInline(text string) string {
// Extract inline code spans first (protect their content)
var spans []string
text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string {
inner := html.EscapeString(m[1 : len(m)-1])
spans = append(spans, "<code>"+inner+"</code>")
return fmt.Sprintf("\x00%d\x00", len(spans)-1)
})
// HTML-escape remaining text (*, _, ~, [, ], (, ) are not HTML special chars)
text = escapeNonPlaceholders(text)
// Apply formatting (order matters)
text = reLink.ReplaceAllString(text, `<a href="$2">$1</a>`)
text = reStrike.ReplaceAllStringFunc(text, func(m string) string {
return "<s>" + m[2:len(m)-2] + "</s>"
})
text = reBoldStar.ReplaceAllStringFunc(text, func(m string) string {
return "<b>" + m[2:len(m)-2] + "</b>"
})
text = reBoldUnder.ReplaceAllStringFunc(text, func(m string) string {
return "<b>" + m[2:len(m)-2] + "</b>"
})
text = reItalicStar.ReplaceAllStringFunc(text, func(m string) string {
return "<i>" + m[1:len(m)-1] + "</i>"
})
text = reItalicUnder.ReplaceAllStringFunc(text, func(m string) string {
return "<i>" + m[1:len(m)-1] + "</i>"
})
// Restore code spans
text = rePlaceholder.ReplaceAllStringFunc(text, func(m string) string {
var idx int
fmt.Sscanf(m[1:len(m)-1], "%d", &idx)
if idx < len(spans) {
return spans[idx]
}
return m
})
return text
}
func escapeNonPlaceholders(text string) string {
locs := rePlaceholder.FindAllStringIndex(text, -1)
if len(locs) == 0 {
return html.EscapeString(text)
}
var sb strings.Builder
last := 0
for _, loc := range locs {
sb.WriteString(html.EscapeString(text[last:loc[0]]))
sb.WriteString(text[loc[0]:loc[1]])
last = loc[1]
}
sb.WriteString(html.EscapeString(text[last:]))
return sb.String()
}