]> git.giacomolli.uk - onechat/blobdiff - telegram_markdown.go
Retry the first WhatsApp connection
[onechat] / telegram_markdown.go
index 54bfbcb36031565e89fa67621ceaf2f78e480d92..8738bcdb38f57e673bd195e11d630505bbee0074 100644 (file)
@@ -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("&", "&amp;", "<", "&lt;")
+
+       // telegramTagUnescaper reverses telegramTagEscaper.
+       telegramTagUnescaper = strings.NewReplacer("&amp;", "&", "&lt;", "<")
+)
+
 // 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 &amp; and each < as &lt;. 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 &amp; or
+// &lt;. 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.