// 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 puts a backslash before each < of markdown outside inline
-// code and fenced code blocks, so that rich Markdown shows a tag name as text
-// and reads no raw HTML. A < that a backslash already makes plain stays as it
-// is. A code block that does not close runs to the end, as in Markdown.
+// 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 {
- 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 == "" {
- telegramEscapeLineTags(&out, line)
- 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 telegramRewriteProse(markdown, telegramTagEscaper.Replace)
+}
- return out.String()
+// 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
return rest
}
-// telegramEscapeLineTags writes line to out with a backslash before each < that
-// is outside inline code and not already plain. A run of backticks that opens
-// no inline code stays as text whole, as in Markdown.
-func telegramEscapeLineTags(out *strings.Builder, line string) {
+// 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 {
- out.WriteString(line[index : index+2])
index += 2
-
continue
}
case '`':
end = span.end
}
+ out.WriteString(rewrite(line[prose:index]))
out.WriteString(line[index:end])
index = end
+ prose = end
continue
- case '<':
- out.WriteByte('\\')
}
- out.WriteByte(line[index])
index++
}
+
+ out.WriteString(rewrite(line[prose:]))
}
// telegramMarkSpan returns the inline mark that starts at index of line and