9 // telegramTagEscaper writes the characters that rich Markdown reads as HTML
11 telegramTagEscaper = strings.NewReplacer("&", "&", "<", "<")
13 // telegramTagUnescaper reverses telegramTagEscaper.
14 telegramTagUnescaper = strings.NewReplacer("&", "&", "<", "<")
17 // telegramMarkdownEntities converts the Markdown of a reply to plain text and
18 // Telegram message entities. It knows bold (** and __), italic (* and _),
19 // strikethrough (~~), inline code, fenced code blocks with an optional
20 // language, links [text](url), and ATX headings, which become bold. Lists,
21 // quotes, and tables stay as their text. A mark that does not close stays as
22 // text with no entity, and code holds no entity.
24 // Inline marks do not cross a line break, so a mark that does not close cannot
25 // change the rest of the reply.
26 func telegramMarkdownEntities(markdown string) (string, []TelegramEntity) {
27 var out TelegramMarkdownText
29 lines := strings.Split(markdown, "\n")
30 for index := 0; index < len(lines); index++ {
36 language, fence := markdownCodeFence(line)
37 end := markdownClosingFence(lines, index, fence)
38 heading := markdownHeadingText(line)
42 out.add(strings.Join(lines[index+1:end], "\n"))
43 out.entity(TelegramEntity{
46 Length: out.units - start,
54 out.entity(TelegramEntity{
57 Length: out.units - start,
64 slices.SortStableFunc(out.entities, func(a, b TelegramEntity) int {
65 if a.Offset != b.Offset {
66 return a.Offset - b.Offset
69 return b.Length - a.Length
72 return out.text.String(), out.entities
75 // telegramEscapeTags writes each & of markdown outside inline code and fenced
76 // code blocks as & and each < as <. Rich Markdown reads raw HTML and
77 // HTML entities in prose, and it does not read a backslash before < as an
78 // escape. With the entities, it shows a tag name and an entity that the model
79 // wrote in prose as text.
80 func telegramEscapeTags(markdown string) string {
81 return telegramRewriteProse(markdown, telegramTagEscaper.Replace)
84 // telegramUnescapeTags gives back the Markdown that telegramEscapeTags escaped.
85 // It is exact, because after the escape each & outside code starts & or
86 // <. It also works on a part of telegramSplitRich, because a part has the
87 // same code as the text that it comes from.
88 func telegramUnescapeTags(markdown string) string {
89 return telegramRewriteProse(markdown, telegramTagUnescaper.Replace)
92 // telegramUTF16Units returns the number of UTF-16 code units of text.
93 func telegramUTF16Units(text string) int {
95 for _, char := range text {
105 // TelegramMarkdownText is the plain text of a Markdown reply and its Telegram
106 // entities, while the conversion builds it. Units counts the UTF-16 code units
107 // of text, the unit of the entity offsets and lengths.
108 type TelegramMarkdownText struct {
111 entities []TelegramEntity
115 func (m *TelegramMarkdownText) add(text string) {
116 m.text.WriteString(text)
118 m.units += telegramUTF16Units(text)
121 // entity keeps entity, unless it is empty, because Telegram refuses an entity
123 func (m *TelegramMarkdownText) entity(entity TelegramEntity) {
124 if entity.Length > 0 {
125 m.entities = append(m.entities, entity)
129 // inline appends one line of Markdown with its inline marks as entities. A
130 // backslash before an ASCII punctuation character makes the character plain
132 func (m *TelegramMarkdownText) inline(line string) {
135 for index < len(line) {
136 if line[index] == '\\' && index+1 < len(line) && strings.IndexByte(markdownPunctuation, line[index+1]) >= 0 {
137 m.add(line[plain:index])
145 span, found := markdownMarkSpan(line, index)
151 m.add(line[plain:index])
154 if span.kind == "code" {
157 m.inline(span.content)
160 m.entity(TelegramEntity{
163 Length: m.units - start,
174 // telegramRewriteProse returns markdown with rewrite applied to each part of a
175 // line outside inline code and fenced code blocks. A code block that does not
176 // close runs to the end, as in Markdown.
177 func telegramRewriteProse(markdown string, rewrite func(string) string) string {
178 var out strings.Builder
180 lines := strings.Split(markdown, "\n")
181 for index := 0; index < len(lines); index++ {
183 out.WriteString("\n")
187 _, fence := markdownCodeFence(line)
189 telegramRewriteLineProse(&out, line, rewrite)
194 end := markdownClosingFence(lines, index, fence)
199 out.WriteString(strings.Join(lines[index:end+1], "\n"))
207 // telegramRewriteLineProse writes line to out with rewrite applied to each part
208 // outside inline code. A backtick after a backslash opens no code. A run of
209 // backticks that opens no inline code stays as text whole, as in Markdown.
210 func telegramRewriteLineProse(out *strings.Builder, line string, rewrite func(string) string) {
212 for index := 0; index < len(line); {
215 if index+1 < len(line) && strings.IndexByte(markdownPunctuation, line[index+1]) >= 0 {
220 end := index + markdownBackticks(line[index:])
221 span, found := markdownCodeSpan(line, index)
226 out.WriteString(rewrite(line[prose:index]))
227 out.WriteString(line[index:end])
238 out.WriteString(rewrite(line[prose:]))