X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/09cde69fdffe2721434392036f98dc241177f5a1..cb514b662bcf9d90b2cf02789c989f777d774d3f:/telegram_markdown.go?ds=inline diff --git a/telegram_markdown.go b/telegram_markdown.go index 54bfbcb..8738bcd 100644 --- a/telegram_markdown.go +++ b/telegram_markdown.go @@ -11,6 +11,15 @@ import ( // 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 @@ -68,6 +77,23 @@ func telegramMarkdownEntities(markdown string) (string, []TelegramEntity) { 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) { @@ -225,6 +251,69 @@ func telegramHeadingText(line 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.