// plain.
const telegramMarkdownPunctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
+var (
+ // telegramTagEscaper writes the characters that rich Markdown reads as HTML
+ // as HTML entities.
+ telegramTagEscaper = strings.NewReplacer("&", "&", "<", "<")
+
+ // telegramTagUnescaper reverses telegramTagEscaper.
+ telegramTagUnescaper = strings.NewReplacer("&", "&", "<", "<")
+)
+
// telegramMarkdownEntities converts the Markdown of a reply to plain text and
// Telegram message entities. It knows bold (** and __), italic (* and _),
// strikethrough (~~), inline code, fenced code blocks with an optional
return out.text.String(), out.entities
}
+// telegramEscapeTags writes each & of markdown outside inline code and fenced
+// code blocks as & and each < as <. Rich Markdown reads raw HTML and
+// HTML entities in prose, and it does not read a backslash before < as an
+// escape. With the entities, it shows a tag name and an entity that the model
+// wrote in prose as text.
+func telegramEscapeTags(markdown string) string {
+ return telegramRewriteProse(markdown, telegramTagEscaper.Replace)
+}
+
+// telegramUnescapeTags gives back the Markdown that telegramEscapeTags escaped.
+// It is exact, because after the escape each & outside code starts & or
+// <. It also works on a part of telegramSplitRich, because a part has the
+// same code as the text that it comes from.
+func telegramUnescapeTags(markdown string) string {
+ return telegramRewriteProse(markdown, telegramTagUnescaper.Replace)
+}
+
// telegramCodeFence returns the language of a line that opens a fenced code
// block, and the fence. The fence is "" when line does not open a block.
func telegramCodeFence(line string) (string, string) {
return rest
}
+// telegramRewriteProse returns markdown with rewrite applied to each part of a
+// line outside inline code and fenced code blocks. A code block that does not
+// close runs to the end, as in Markdown.
+func telegramRewriteProse(markdown string, rewrite func(string) string) string {
+ var out strings.Builder
+ lines := strings.Split(markdown, "\n")
+ for index := 0; index < len(lines); index++ {
+ if index > 0 {
+ out.WriteString("\n")
+ }
+
+ line := lines[index]
+ _, fence := telegramCodeFence(line)
+ if fence == "" {
+ telegramRewriteLineProse(&out, line, rewrite)
+ continue
+ }
+
+ end := telegramClosingFence(lines, index, fence)
+ if end < 0 {
+ end = len(lines) - 1
+ }
+
+ out.WriteString(strings.Join(lines[index:end+1], "\n"))
+ index = end
+ }
+
+ return out.String()
+}
+
+// telegramRewriteLineProse writes line to out with rewrite applied to each part
+// outside inline code. A backtick after a backslash opens no code. A run of
+// backticks that opens no inline code stays as text whole, as in Markdown.
+func telegramRewriteLineProse(out *strings.Builder, line string, rewrite func(string) string) {
+ prose := 0
+ for index := 0; index < len(line); {
+ switch line[index] {
+ case '\\':
+ if index+1 < len(line) && strings.IndexByte(telegramMarkdownPunctuation, line[index+1]) >= 0 {
+ index += 2
+ continue
+ }
+ case '`':
+ end := index + telegramBackticks(line[index:])
+ span, found := telegramCodeSpan(line, index)
+ if found {
+ end = span.end
+ }
+
+ out.WriteString(rewrite(line[prose:index]))
+ out.WriteString(line[index:end])
+ index = end
+ prose = end
+
+ continue
+ }
+
+ index++
+ }
+
+ out.WriteString(rewrite(line[prose:]))
+}
+
// telegramMarkSpan returns the inline mark that starts at index of line and
// closes in line. It returns false when no mark starts there or the mark does
// not close.