REPOSITORY / ScuroNeko/Laniakea
Pull Requests
v1.0.0 #9
@@ -1,6 +1,6 @@
|
||||
name: Golang lint
|
||||
run-name: Linting code
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -10,10 +10,19 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Verify formatting
|
||||
run: test -z "$(gofmt -l .)"
|
||||
run: |
|
||||
files="$(gofmt -l .)"
|
||||
if [ -n "$files" ]; then
|
||||
echo "These files are not gofmt-formatted:"
|
||||
echo "$files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run go test
|
||||
run: go test ./...
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Run golangci-lint
|
||||
run: golangci-lint run
|
||||
|
||||
@@ -5,7 +5,6 @@ linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
|
||||
+3
-1
@@ -13,12 +13,13 @@
|
||||
- Added `MsgContext.IsCallback()` and `MsgContext.HasPhoto()` helpers for callback-aware handler code.
|
||||
- Added `MsgContext.UpsertKeyboard(...)` and `MsgContext.UpsertKeyboardMarkdown(...)` helpers that edit callback messages, replace photo callback messages with a fresh chat message, and send a new chat message outside callback flow.
|
||||
- Added `CommandGroup`, `NewCommandGroup(...)`, `Plugin.CommandGroup(...)`, and `Plugin.AddCommandGroup(...)` helpers for registering prefixed command groups with shared middleware.
|
||||
- Added the `tgmd` package with Telegram Markdown formatting helpers and a message entity builder.
|
||||
- Added the `tgfmt` package with typed MarkdownV2, HTML, legacy Markdown formatting helpers, and a message entity builder.
|
||||
|
||||
### Changed
|
||||
- Version metadata now reports the stable `v1.0.0` release instead of `v1.0.0-rc.16`.
|
||||
- Bot-level middleware blocks now emit a final `UpdateHandledEvent` with `Handled=false`, keeping observer update lifecycles balanced.
|
||||
- `BotOpts`, `tgapi.APIOpts`, logger utilities, README, and wiki pages now document the final stable API names and configuration options consistently.
|
||||
- CI now checks formatting, tests, vet, and lint on both pushes and pull requests.
|
||||
|
||||
### Fixed
|
||||
- Fixed webhook startup so empty-secret warnings are logged only after the webhook logger is initialized.
|
||||
@@ -29,6 +30,7 @@
|
||||
- Added webhook runtime regression coverage for request enqueue through worker execution of a command handler.
|
||||
- Added regression coverage for inline callback keyboard upserts and callback target detection.
|
||||
- Added regression coverage for command group prefixing, middleware order, clone behavior, and plugin registration.
|
||||
- Added formatting coverage for escaping, composition, link destinations, HTML attributes, and legacy Markdown code blocks.
|
||||
|
||||
## v1.0.0-rc.16
|
||||
|
||||
|
||||
+11
-11
@@ -139,7 +139,7 @@ func (m *AnswerMessage) Edit(text string) *AnswerMessage {
|
||||
|
||||
// EditMarkdown replaces the text of the message using MarkdownV2 formatting.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
// Unescaped input may cause Telegram API errors or broken formatting.
|
||||
func (m *AnswerMessage) EditMarkdown(text string) *AnswerMessage {
|
||||
return m.ctx.edit(m.MessageID, text, nil, tgapi.ParseMarkdownV2)
|
||||
@@ -161,7 +161,7 @@ func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *Answ
|
||||
|
||||
// EditCallbackMarkdown edits the callback message using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
|
||||
return ctx.editCallback(text, keyboard, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, ar
|
||||
|
||||
// EditCallbackfMarkdown formats a string using fmt.Sprintf and edits the callback message with MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) EditCallbackfMarkdown(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage {
|
||||
return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (m *AnswerMessage) EditCaption(text string) *AnswerMessage {
|
||||
|
||||
// EditCaptionMarkdown edits the caption of a media message using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (m *AnswerMessage) EditCaptionMarkdown(text string) *AnswerMessage {
|
||||
return m.ctx.editPhotoText(m.MessageID, text, nil, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *An
|
||||
|
||||
// EditCaptionKeyboardMarkdown edits the caption of a media message with a new inline keyboard using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeyboard) *AnswerMessage {
|
||||
return m.ctx.editPhotoText(m.MessageID, text, kb, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func (ctx *MsgContext) AnswerLong(text string) []*AnswerMessage {
|
||||
|
||||
// AnswerMarkdown sends a message using MarkdownV2 formatting.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) AnswerMarkdown(text string) *AnswerMessage {
|
||||
return ctx.answer(text, nil, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func (ctx *MsgContext) AnswerLongf(template string, args ...any) []*AnswerMessag
|
||||
|
||||
// AnswerfMarkdown formats a string using fmt.Sprintf and sends it using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) AnswerfMarkdown(template string, args ...any) *AnswerMessage {
|
||||
return ctx.answer(fmt.Sprintf(template, args...), nil, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func (ctx *MsgContext) KeyboardLong(text string, kb *InlineKeyboard) []*AnswerMe
|
||||
|
||||
// KeyboardMarkdown sends a message with an inline keyboard using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
|
||||
return ctx.answer(text, keyboard, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -413,7 +413,7 @@ func (ctx *MsgContext) AnswerPhoto(photoID, text string) *AnswerMessage {
|
||||
|
||||
// AnswerPhotoMarkdown sends a photo with MarkdownV2 caption.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) AnswerPhotoMarkdown(photoID, text string) *AnswerMessage {
|
||||
return ctx.answerPhoto(photoID, text, nil, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -425,7 +425,7 @@ func (ctx *MsgContext) AnswerPhotoKeyboard(photoID, text string, kb *InlineKeybo
|
||||
|
||||
// AnswerPhotoKeyboardMarkdown sends a photo with caption and inline keyboard using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) AnswerPhotoKeyboardMarkdown(photoID, text string, kb *InlineKeyboard) *AnswerMessage {
|
||||
return ctx.answerPhoto(photoID, text, kb, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
@@ -437,7 +437,7 @@ func (ctx *MsgContext) AnswerPhotof(photoID, template string, args ...any) *Answ
|
||||
|
||||
// AnswerPhotofMarkdown formats a string and sends it as a photo caption using MarkdownV2.
|
||||
//
|
||||
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
|
||||
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
|
||||
func (ctx *MsgContext) AnswerPhotofMarkdown(photoID, template string, args ...any) *AnswerMessage {
|
||||
return ctx.answerPhoto(photoID, fmt.Sprintf(template, args...), nil, tgapi.ParseMarkdownV2)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package tgfmt provides small helpers for Telegram text formatting.
|
||||
package tgfmt
|
||||
@@ -0,0 +1,99 @@
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTML is an escaped Telegram HTML fragment.
|
||||
//
|
||||
// Methods on HTML compose formatting without escaping the fragment again.
|
||||
type HTML string
|
||||
|
||||
// EscapeHTML escapes special characters for Telegram HTML parse mode.
|
||||
func EscapeHTML(s string) HTML {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, `"`, """)
|
||||
return HTML(s)
|
||||
}
|
||||
|
||||
// Bold returns h wrapped as bold Telegram HTML text.
|
||||
func (h HTML) Bold() HTML {
|
||||
return "<b>" + h + "</b>"
|
||||
}
|
||||
|
||||
// Italic returns h wrapped as italic Telegram HTML text.
|
||||
func (h HTML) Italic() HTML {
|
||||
return "<i>" + h + "</i>"
|
||||
}
|
||||
|
||||
// Underline returns h wrapped as underlined Telegram HTML text.
|
||||
func (h HTML) Underline() HTML {
|
||||
return "<u>" + h + "</u>"
|
||||
}
|
||||
|
||||
// Strikethrough returns h wrapped as strikethrough Telegram HTML text.
|
||||
func (h HTML) Strikethrough() HTML {
|
||||
return "<s>" + h + "</s>"
|
||||
}
|
||||
|
||||
// Spoiler returns h wrapped as spoiler Telegram HTML text.
|
||||
func (h HTML) Spoiler() HTML {
|
||||
return "<tg-spoiler>" + h + "</tg-spoiler>"
|
||||
}
|
||||
|
||||
// Link returns h as a Telegram HTML text link.
|
||||
func (h HTML) Link(url string) HTML {
|
||||
return `<a href="` + escapeHTMLAttr(url) + `">` + h + "</a>"
|
||||
}
|
||||
|
||||
// Mention returns h as a Telegram HTML user mention.
|
||||
func (h HTML) Mention(userID int64) HTML {
|
||||
return `<a href="tg://user?id=` + HTML(strconv.FormatInt(userID, 10)) + `">` + h + "</a>"
|
||||
}
|
||||
|
||||
// Emoji returns h as a Telegram HTML custom emoji.
|
||||
func (h HTML) Emoji(emojiID string) HTML {
|
||||
return `<tg-emoji emoji-id="` + escapeHTMLAttr(emojiID) + `">` + h + "</tg-emoji>"
|
||||
}
|
||||
|
||||
// Time returns h as a Telegram HTML localized timestamp.
|
||||
func (h HTML) Time(unix int64) HTML {
|
||||
return `<tg-time unix="` + HTML(strconv.FormatInt(unix, 10)) + `">` + h + "</tg-time>"
|
||||
}
|
||||
|
||||
// TimeFormat returns h as a Telegram HTML localized timestamp with format.
|
||||
func (h HTML) TimeFormat(unix int64, format string) HTML {
|
||||
return `<tg-time unix="` + HTML(strconv.FormatInt(unix, 10)) + `" format="` + escapeHTMLAttr(format) + `">` + h + "</tg-time>"
|
||||
}
|
||||
|
||||
// InlineCode returns h wrapped as inline code Telegram HTML text.
|
||||
func (h HTML) InlineCode() HTML {
|
||||
return "<code>" + h + "</code>"
|
||||
}
|
||||
|
||||
// BlockCode returns h wrapped as a Telegram HTML code block.
|
||||
func (h HTML) BlockCode() HTML {
|
||||
return "<pre>" + h + "</pre>"
|
||||
}
|
||||
|
||||
// BlockCodeLanguage returns h wrapped as a Telegram HTML code block with language.
|
||||
func (h HTML) BlockCodeLanguage(lang string) HTML {
|
||||
return `<pre><code class="language-` + escapeHTMLAttr(lang) + `">` + h + "</code></pre>"
|
||||
}
|
||||
|
||||
// Quote returns h as a Telegram HTML blockquote.
|
||||
func (h HTML) Quote() HTML {
|
||||
return "<blockquote>" + h + "</blockquote>"
|
||||
}
|
||||
|
||||
// QuoteExpandable returns h as a Telegram HTML expandable blockquote.
|
||||
func (h HTML) QuoteExpandable() HTML {
|
||||
return "<blockquote expandable>" + h + "</blockquote>"
|
||||
}
|
||||
|
||||
func escapeHTMLAttr(s string) HTML {
|
||||
return EscapeHTML(s)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package tgfmt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEscapeHTML(t *testing.T) {
|
||||
got := EscapeHTML(`<tag attr="a&b">`)
|
||||
want := HTML(`<tag attr="a&b">`)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("EscapeHTML() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLComposesWithoutDoubleEscaping(t *testing.T) {
|
||||
got := EscapeHTML("<b>").Bold().Italic()
|
||||
want := HTML("<i><b><b></b></i>")
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("formatted HTML = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLFormattingMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got HTML
|
||||
want HTML
|
||||
}{
|
||||
{name: "bold", got: EscapeHTML("text").Bold(), want: "<b>text</b>"},
|
||||
{name: "italic", got: EscapeHTML("text").Italic(), want: "<i>text</i>"},
|
||||
{name: "underline", got: EscapeHTML("text").Underline(), want: "<u>text</u>"},
|
||||
{name: "strikethrough", got: EscapeHTML("text").Strikethrough(), want: "<s>text</s>"},
|
||||
{name: "spoiler", got: EscapeHTML("text").Spoiler(), want: "<tg-spoiler>text</tg-spoiler>"},
|
||||
{name: "inline code", got: EscapeHTML("text").InlineCode(), want: "<code>text</code>"},
|
||||
{name: "block code", got: EscapeHTML("text").BlockCode(), want: "<pre>text</pre>"},
|
||||
{name: "block code language", got: EscapeHTML("text").BlockCodeLanguage(`go"`), want: `<pre><code class="language-go"">text</code></pre>`},
|
||||
{name: "quote", got: EscapeHTML("text").Quote(), want: "<blockquote>text</blockquote>"},
|
||||
{name: "expandable quote", got: EscapeHTML("text").QuoteExpandable(), want: "<blockquote expandable>text</blockquote>"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("formatted HTML = %q, want %q", tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLLinkEscapesAttributes(t *testing.T) {
|
||||
got := EscapeHTML("Laniakea").Link(`https://example.test/?q="a&b"`)
|
||||
want := HTML(`<a href="https://example.test/?q="a&b"">Laniakea</a>`)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("Link() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLSpecialLinks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got HTML
|
||||
want HTML
|
||||
}{
|
||||
{name: "mention", got: EscapeHTML("User").Mention(123), want: `<a href="tg://user?id=123">User</a>`},
|
||||
{name: "emoji", got: EscapeHTML("emoji").Emoji(`12"3`), want: `<tg-emoji emoji-id="12"3">emoji</tg-emoji>`},
|
||||
{name: "time", got: EscapeHTML("date").Time(1772323200), want: `<tg-time unix="1772323200">date</tg-time>`},
|
||||
{name: "time format", got: EscapeHTML("date").TimeFormat(1772323200, `MMM " yyyy`), want: `<tg-time unix="1772323200" format="MMM " yyyy">date</tg-time>`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("formatted HTML link = %q, want %q", tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Markdown is an escaped legacy Telegram Markdown fragment.
|
||||
//
|
||||
// Deprecated: Use MarkdownV2 instead.
|
||||
type Markdown string
|
||||
|
||||
// EscapeMarkdown escapes special characters for legacy Telegram Markdown.
|
||||
//
|
||||
// Deprecated: Use EscapeMarkdownV2 instead.
|
||||
func EscapeMarkdown(s string) Markdown {
|
||||
s = strings.ReplaceAll(s, "_", `\_`)
|
||||
s = strings.ReplaceAll(s, "*", `\*`)
|
||||
s = strings.ReplaceAll(s, "[", `\[`)
|
||||
return Markdown(strings.ReplaceAll(s, "`", "\\`"))
|
||||
}
|
||||
|
||||
// Bold returns s wrapped as bold legacy Telegram Markdown text.
|
||||
func (s Markdown) Bold() Markdown {
|
||||
return "*" + s + "*"
|
||||
}
|
||||
|
||||
// Italic returns s wrapped as italic legacy Telegram Markdown text.
|
||||
func (s Markdown) Italic() Markdown {
|
||||
return "_" + s + "_"
|
||||
}
|
||||
|
||||
// Link returns s as a legacy Telegram Markdown text link.
|
||||
func (s Markdown) Link(url string) Markdown {
|
||||
return "[" + s + "](" + Markdown(url) + ")"
|
||||
}
|
||||
|
||||
// Mention returns s as a legacy Telegram Markdown user mention.
|
||||
func (s Markdown) Mention(userID int64) Markdown {
|
||||
return "[" + s + "](tg://user?id=" + Markdown(strconv.FormatInt(userID, 10)) + ")"
|
||||
}
|
||||
|
||||
// InlineCode returns s wrapped as inline code legacy Telegram Markdown text.
|
||||
func (s Markdown) InlineCode() Markdown {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
|
||||
// BlockCode returns s wrapped as a legacy Telegram Markdown code block.
|
||||
func (s Markdown) BlockCode() Markdown {
|
||||
return "```\n" + s + "\n```"
|
||||
}
|
||||
|
||||
// BlockCodeLanguage returns s wrapped as a legacy Telegram Markdown code block.
|
||||
func (s Markdown) BlockCodeLanguage(lang string) Markdown {
|
||||
return "```" + Markdown(lang) + "\n" + s + "\n```"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tgfmt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEscapeMarkdown(t *testing.T) {
|
||||
got := EscapeMarkdown("a_b*c[1]`x`")
|
||||
want := Markdown("a\\_b\\*c\\[1]\\`x\\`")
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("EscapeMarkdown() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownFormattingMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got Markdown
|
||||
want Markdown
|
||||
}{
|
||||
{name: "bold", got: EscapeMarkdown("text").Bold(), want: "*text*"},
|
||||
{name: "italic", got: EscapeMarkdown("text").Italic(), want: "_text_"},
|
||||
{name: "link", got: EscapeMarkdown("Laniakea").Link("https://example.test"), want: "[Laniakea](https://example.test)"},
|
||||
{name: "mention", got: EscapeMarkdown("User").Mention(123), want: "[User](tg://user?id=123)"},
|
||||
{name: "inline code", got: EscapeMarkdown("text").InlineCode(), want: "`text`"},
|
||||
{name: "block code", got: EscapeMarkdown("text").BlockCode(), want: "```\ntext\n```"},
|
||||
{name: "block code language", got: EscapeMarkdown("text").BlockCodeLanguage("go"), want: "```go\ntext\n```"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("formatted Markdown = %q, want %q", tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarkdownV2 is an escaped Telegram MarkdownV2 fragment.
|
||||
//
|
||||
// Methods on MarkdownV2 compose formatting without escaping the fragment again.
|
||||
type MarkdownV2 string
|
||||
|
||||
// EscapeMarkdownV2 escapes special characters for Telegram MarkdownV2.
|
||||
// https://core.telegram.org/bots/api#markdownv2-style
|
||||
func EscapeMarkdownV2(s string) MarkdownV2 {
|
||||
symbols := []string{"\\", "_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||
for _, symbol := range symbols {
|
||||
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
|
||||
}
|
||||
return MarkdownV2(s)
|
||||
}
|
||||
|
||||
// Bold returns s wrapped as bold Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) Bold() MarkdownV2 {
|
||||
return "*" + s + "*"
|
||||
}
|
||||
|
||||
// Italic returns s wrapped as italic Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) Italic() MarkdownV2 {
|
||||
return "_" + s + "_"
|
||||
}
|
||||
|
||||
// Underline returns s wrapped as underlined Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) Underline() MarkdownV2 {
|
||||
return "__" + s + "__"
|
||||
}
|
||||
|
||||
// Strikethrough returns s wrapped as strikethrough Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) Strikethrough() MarkdownV2 {
|
||||
return "~" + s + "~"
|
||||
}
|
||||
|
||||
// Spoiler returns s wrapped as spoiler Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) Spoiler() MarkdownV2 {
|
||||
return "||" + s + "||"
|
||||
}
|
||||
|
||||
// Link returns s as a Telegram MarkdownV2 text link.
|
||||
func (s MarkdownV2) Link(url string) MarkdownV2 {
|
||||
return "[" + s + "](" + escapeMarkdownV2LinkDestination(url) + ")"
|
||||
}
|
||||
|
||||
// Mention returns s as a Telegram MarkdownV2 user mention.
|
||||
func (s MarkdownV2) Mention(userID uint64) MarkdownV2 {
|
||||
return "[" + s + "](tg://user?id=" + MarkdownV2(strconv.FormatUint(userID, 10)) + ")"
|
||||
}
|
||||
|
||||
// Emoji returns s as a Telegram MarkdownV2 custom emoji.
|
||||
func (s MarkdownV2) Emoji(emojiID string) MarkdownV2 {
|
||||
return "[" + s + "](tg://emoji?id=" + escapeMarkdownV2LinkDestination(emojiID) + ")"
|
||||
}
|
||||
|
||||
// Time returns s as a Telegram MarkdownV2 localized timestamp.
|
||||
func (s MarkdownV2) Time(unix uint64) MarkdownV2 {
|
||||
return ") + ")"
|
||||
}
|
||||
|
||||
// TimeFormat returns s as a Telegram MarkdownV2 localized timestamp with format.
|
||||
func (s MarkdownV2) TimeFormat(unix uint64, format string) MarkdownV2 {
|
||||
dest := "tg://time?unix=" + strconv.FormatUint(unix, 10) + "&format=" + format
|
||||
return " + ")"
|
||||
}
|
||||
|
||||
// InlineCode returns s wrapped as inline code Telegram MarkdownV2 text.
|
||||
func (s MarkdownV2) InlineCode() MarkdownV2 {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
|
||||
// BlockCode returns s wrapped as a Telegram MarkdownV2 code block.
|
||||
func (s MarkdownV2) BlockCode() MarkdownV2 {
|
||||
return "```\n" + s + "\n```"
|
||||
}
|
||||
|
||||
// BlockCodeLanguage returns s wrapped as a Telegram MarkdownV2 code block with language.
|
||||
func (s MarkdownV2) BlockCodeLanguage(lang string) MarkdownV2 {
|
||||
return "```" + MarkdownV2(lang) + "\n" + s + "\n```"
|
||||
}
|
||||
|
||||
// Quote returns s as a Telegram MarkdownV2 blockquote.
|
||||
func (s MarkdownV2) Quote() MarkdownV2 {
|
||||
return MarkdownV2(">" + strings.ReplaceAll(string(s), "\n", "\n>"))
|
||||
}
|
||||
|
||||
// QuoteExpandable returns s as a Telegram MarkdownV2 expandable blockquote.
|
||||
func (s MarkdownV2) QuoteExpandable() MarkdownV2 {
|
||||
return "**>" + s
|
||||
}
|
||||
|
||||
func escapeMarkdownV2LinkDestination(s string) MarkdownV2 {
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
s = strings.ReplaceAll(s, ")", "\\)")
|
||||
return MarkdownV2(s)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package tgfmt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEscapeMarkdownV2(t *testing.T) {
|
||||
got := EscapeMarkdownV2(`a_b*c[1](x)!`)
|
||||
want := MarkdownV2(`a\_b\*c\[1\]\(x\)\!`)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("EscapeMarkdownV2() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownV2ComposesWithoutDoubleEscaping(t *testing.T) {
|
||||
got := EscapeMarkdownV2("a*b").Bold().Italic()
|
||||
want := MarkdownV2(`_*a\*b*_`)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("formatted text = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownV2FormattingMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got MarkdownV2
|
||||
want MarkdownV2
|
||||
}{
|
||||
{name: "bold", got: EscapeMarkdownV2("text").Bold(), want: "*text*"},
|
||||
{name: "italic", got: EscapeMarkdownV2("text").Italic(), want: "_text_"},
|
||||
{name: "underline", got: EscapeMarkdownV2("text").Underline(), want: "__text__"},
|
||||
{name: "strikethrough", got: EscapeMarkdownV2("text").Strikethrough(), want: "~text~"},
|
||||
{name: "spoiler", got: EscapeMarkdownV2("text").Spoiler(), want: "||text||"},
|
||||
{name: "inline code", got: EscapeMarkdownV2("text").InlineCode(), want: "`text`"},
|
||||
{name: "block code", got: EscapeMarkdownV2("text").BlockCode(), want: "```\ntext\n```"},
|
||||
{name: "block code language", got: EscapeMarkdownV2("text").BlockCodeLanguage("go"), want: "```go\ntext\n```"},
|
||||
{name: "quote", got: EscapeMarkdownV2("a\nb").Quote(), want: ">a\n>b"},
|
||||
{name: "expandable quote", got: EscapeMarkdownV2("text").QuoteExpandable(), want: "**>text"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("formatted text = %q, want %q", tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownV2LinkEscapesDestination(t *testing.T) {
|
||||
got := EscapeMarkdownV2("Laniakea").Link(`https://example.test/a)b\c`)
|
||||
want := MarkdownV2(`[Laniakea](https://example.test/a\)b\\c)`)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("Link() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownV2SpecialLinks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got MarkdownV2
|
||||
want MarkdownV2
|
||||
}{
|
||||
{name: "mention", got: EscapeMarkdownV2("User").Mention(123), want: "[User](tg://user?id=123)"},
|
||||
{name: "emoji", got: EscapeMarkdownV2("emoji").Emoji(`12)3`), want: `[emoji](tg://emoji?id=12\)3)`},
|
||||
{name: "time", got: EscapeMarkdownV2("date").Time(1772323200), want: ""},
|
||||
{name: "time format", got: EscapeMarkdownV2("date").TimeFormat(1772323200, `MMM ) yyyy`), want: ` yyyy)`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Fatalf("formatted link = %q, want %q", tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tgmd
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
//TODO GoDoc, tests. Maybe escape Markdown v2
|
||||
|
||||
// MessageBuilder builds Telegram message text with explicit message entities.
|
||||
// MessageBuilder is not safe for concurrent use.
|
||||
type MessageBuilder struct {
|
||||
@@ -46,6 +44,7 @@ func (b *MessageBuilder) Entities() []tgapi.MessageEntity {
|
||||
return append([]tgapi.MessageEntity(nil), b.entities...)
|
||||
}
|
||||
|
||||
// Build returns the built message text and a copy of its entities.
|
||||
func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
|
||||
if b.isDirty {
|
||||
b.update()
|
||||
@@ -53,6 +52,7 @@ func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
|
||||
return b.str, append([]tgapi.MessageEntity(nil), b.entities...)
|
||||
}
|
||||
|
||||
// Reset clears the builder and keeps it ready for reuse.
|
||||
func (b *MessageBuilder) Reset() {
|
||||
b.str = ""
|
||||
b.offset = 0
|
||||
@@ -67,7 +67,7 @@ func (b *MessageBuilder) update() *MessageBuilder {
|
||||
var textLen int
|
||||
var entitiesLen int
|
||||
for _, e := range b.entries {
|
||||
textLen += len(e.text) // bytes, для Grow нормально
|
||||
textLen += len(e.text)
|
||||
entitiesLen += len(e.entities)
|
||||
}
|
||||
|
||||
@@ -91,10 +91,12 @@ func (b *MessageBuilder) update() *MessageBuilder {
|
||||
b.isDirty = false
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) markDirty() {
|
||||
b.isDirty = true
|
||||
}
|
||||
|
||||
// MessageBuilderEntry represents text appended to a MessageBuilder.
|
||||
type MessageBuilderEntry struct {
|
||||
text string
|
||||
length int
|
||||
@@ -118,6 +120,7 @@ func (b *MessageBuilder) Add(text string) *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// Mention marks the entry as a Telegram mention.
|
||||
func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityMention,
|
||||
@@ -126,6 +129,7 @@ func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// Hashtag marks the entry as a Telegram hashtag.
|
||||
func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityHashtag,
|
||||
@@ -134,6 +138,7 @@ func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// Cashtag marks the entry as a Telegram cashtag.
|
||||
func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCashtag,
|
||||
@@ -142,6 +147,7 @@ func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// BotCommand marks the entry as a Telegram bot command.
|
||||
func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBotCommand,
|
||||
@@ -150,6 +156,7 @@ func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// Email marks the entry as an email address.
|
||||
func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityEmail,
|
||||
@@ -158,6 +165,7 @@ func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
// Phone marks the entry as a phone number.
|
||||
func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPhoneNumber,
|
||||
@@ -165,6 +173,8 @@ func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Bold marks the entry as bold text.
|
||||
func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
@@ -172,6 +182,8 @@ func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Italic marks the entry as italic text.
|
||||
func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityItalic,
|
||||
@@ -179,6 +191,8 @@ func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Underline marks the entry as underlined text.
|
||||
func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityUnderline,
|
||||
@@ -186,6 +200,8 @@ func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Strikethrough marks the entry as strikethrough text.
|
||||
func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityStrike,
|
||||
@@ -193,6 +209,8 @@ func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Spoiler marks the entry as spoiler text.
|
||||
func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntitySpoiler,
|
||||
@@ -200,6 +218,8 @@ func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Quote marks the entry as a blockquote.
|
||||
func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBlockquote,
|
||||
@@ -207,6 +227,8 @@ func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// ExpandableQuote marks the entry as an expandable blockquote.
|
||||
func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityExpandableBlockquote,
|
||||
@@ -214,6 +236,8 @@ func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// InlineCode marks the entry as inline code.
|
||||
func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCode,
|
||||
@@ -221,6 +245,8 @@ func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// CodeBlock marks the entry as a preformatted code block.
|
||||
func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
@@ -228,6 +254,8 @@ func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// CodeBlockWithLanguage marks the entry as a preformatted code block with language.
|
||||
func (e *MessageBuilderEntry) CodeBlockWithLanguage(lang string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
@@ -235,6 +263,8 @@ func (e *MessageBuilderEntry) CodeBlockWithLanguage(lang string) *MessageBuilder
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Link marks the entry as a text link.
|
||||
func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityTextLink,
|
||||
@@ -242,6 +272,8 @@ func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// TextMention marks the entry as a mention of user.
|
||||
func (e *MessageBuilderEntry) TextMention(user *tgapi.User) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityTextMention,
|
||||
@@ -249,6 +281,8 @@ func (e *MessageBuilderEntry) TextMention(user *tgapi.User) *MessageBuilderEntry
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// CustomEmoji marks the entry as a custom emoji.
|
||||
func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCustomEmoji,
|
||||
@@ -256,6 +290,8 @@ func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// DateTime marks the entry as a localized timestamp.
|
||||
func (e *MessageBuilderEntry) DateTime(time time.Time) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityDateTime,
|
||||
@@ -263,6 +299,8 @@ func (e *MessageBuilderEntry) DateTime(time time.Time) *MessageBuilderEntry {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// DateTimeFormat marks the entry as a localized timestamp with format.
|
||||
func (e *MessageBuilderEntry) DateTimeFormat(time time.Time, format string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityDateTime,
|
||||
@@ -1,4 +1,4 @@
|
||||
package tgmd
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -0,0 +1,12 @@
|
||||
package tgfmt
|
||||
|
||||
import "strings"
|
||||
|
||||
// EscapePunctuation escapes '.', '!' and '-' for MarkdownV2 fragments.
|
||||
func EscapePunctuation(s string) string {
|
||||
symbols := []string{".", "!", "-"}
|
||||
for _, symbol := range symbols {
|
||||
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package tgmd provides small helpers for Telegram Markdown text.
|
||||
package tgmd
|
||||
@@ -1,71 +0,0 @@
|
||||
package tgmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea"
|
||||
)
|
||||
|
||||
// Helpers in this file generate Telegram MarkdownV2.
|
||||
// All user-provided text is escaped.
|
||||
|
||||
// TODO Markdown v2 escaping. GoDoc and tests
|
||||
|
||||
// WithBold returns s wrapped as bold Telegram Markdown text.
|
||||
func WithBold(s string) string {
|
||||
return "*" + s + "*"
|
||||
}
|
||||
|
||||
// WithItalic returns s wrapped as italic Telegram Markdown text.
|
||||
func WithItalic(s string) string {
|
||||
return "_" + s + "_"
|
||||
}
|
||||
|
||||
func WithUnderline(s string) string {
|
||||
return "__" + s + "__"
|
||||
}
|
||||
func WithStrikethrough(s string) string {
|
||||
return "~" + s + "~"
|
||||
}
|
||||
func WithSpoiler(s string) string {
|
||||
return "||" + s + "||"
|
||||
}
|
||||
|
||||
// WithLink returns a Telegram Markdown link for text and URL.
|
||||
func WithLink(text, url string) string {
|
||||
return "[" + text + "](" + url + ")"
|
||||
}
|
||||
|
||||
func WithMention(text string, userID uint64) string {
|
||||
return "[" + text + "](tg://user?id=" + strconv.FormatUint(userID, 10) + ")"
|
||||
}
|
||||
func WithEmoji(text, emojiID string) string {
|
||||
return "[" + text + "](tg://emoji?id=" + emojiID + ")"
|
||||
}
|
||||
|
||||
func WithTime(text string, unix uint64) string {
|
||||
return " + ")"
|
||||
}
|
||||
func WithTimeFormat(text string, unix uint64, format string) string {
|
||||
return " +
|
||||
"&format=" + format + ")"
|
||||
}
|
||||
|
||||
// WithInlineCode returns s wrapped as inline code Telegram Markdown text.
|
||||
func WithInlineCode(s string) string {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
func WithBlockCode(s string) string {
|
||||
return "```\n" + s + "\n```"
|
||||
}
|
||||
func WithBlockCodeLanguage(s, lang string) string {
|
||||
return "```" + lang + "\n" + s + "\n```"
|
||||
}
|
||||
func WithQuote(s string) string {
|
||||
return ">" + strings.ReplaceAll(laniakea.EscapeMarkdownV2(s), "\n", "\n>")
|
||||
}
|
||||
func WithQuoteExpandable(s string) string {
|
||||
return "**>" + s
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package tgmd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFormattingHelpers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{name: "bold", got: WithBold("text"), want: "*text*"},
|
||||
{name: "italic", got: WithItalic("text"), want: "_text_"},
|
||||
{name: "inline code", got: WithInlineCode("text"), want: "`text`"},
|
||||
{name: "link", got: WithLink("Laniakea", "https://example.test"), want: "[Laniakea](https://example.test)"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.got != tc.want {
|
||||
t.Fatalf("unexpected formatted text: got %q want %q", tc.got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||
)
|
||||
|
||||
@@ -17,42 +15,6 @@ func Val[T any](p *T, def T) T {
|
||||
return def
|
||||
}
|
||||
|
||||
// EscapeMarkdown escapes special characters for legacy Telegram Markdown.
|
||||
// Deprecated: Use EscapeMarkdownV2.
|
||||
func EscapeMarkdown(s string) string {
|
||||
s = strings.ReplaceAll(s, "_", `\_`)
|
||||
s = strings.ReplaceAll(s, "*", `\*`)
|
||||
s = strings.ReplaceAll(s, "[", `\[`)
|
||||
return strings.ReplaceAll(s, "`", "\\`")
|
||||
}
|
||||
|
||||
// EscapeHTML escapes special characters for Telegram HTML parse mode.
|
||||
func EscapeHTML(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
return s
|
||||
}
|
||||
|
||||
// EscapeMarkdownV2 escapes special characters for Telegram MarkdownV2.
|
||||
// https://core.telegram.org/bots/api#markdownv2-style
|
||||
func EscapeMarkdownV2(s string) string {
|
||||
symbols := []string{"\\", "_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||
for _, symbol := range symbols {
|
||||
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// EscapePunctuation escapes '.', '!' and '-' for MarkdownV2 fragments.
|
||||
func EscapePunctuation(s string) string {
|
||||
symbols := []string{".", "!", "-"}
|
||||
for _, symbol := range symbols {
|
||||
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
// VersionString re-exports the module version string.
|
||||
VersionString = utils.VersionString
|
||||
|
||||
Reference in New Issue
Block a user