This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
// Package tgmd provides small helpers for Telegram Markdown text.
|
||||
package tgmd
|
||||
@@ -0,0 +1,295 @@
|
||||
package tgmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/extypes"
|
||||
"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 {
|
||||
str string
|
||||
offset int
|
||||
entities extypes.Slice[tgapi.MessageEntity]
|
||||
|
||||
entries extypes.Slice[*MessageBuilderEntry]
|
||||
isDirty bool
|
||||
}
|
||||
|
||||
// NewMessageBuilder returns an empty MessageBuilder.
|
||||
func NewMessageBuilder() *MessageBuilder {
|
||||
return &MessageBuilder{
|
||||
entities: make([]tgapi.MessageEntity, 0),
|
||||
entries: make(extypes.Slice[*MessageBuilderEntry], 0),
|
||||
isDirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the built message text.
|
||||
func (b *MessageBuilder) String() string {
|
||||
if b.isDirty {
|
||||
b.update()
|
||||
}
|
||||
return b.str
|
||||
}
|
||||
|
||||
// Entities returns a copy of the built message entities.
|
||||
func (b *MessageBuilder) Entities() []tgapi.MessageEntity {
|
||||
if b.isDirty {
|
||||
b.update()
|
||||
}
|
||||
return append([]tgapi.MessageEntity(nil), b.entities...)
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
|
||||
if b.isDirty {
|
||||
b.update()
|
||||
}
|
||||
return b.str, append([]tgapi.MessageEntity(nil), b.entities...)
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) Reset() {
|
||||
b.str = ""
|
||||
b.offset = 0
|
||||
b.entities = b.entities[:0]
|
||||
b.entries = b.entries[:0]
|
||||
b.isDirty = false
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) update() *MessageBuilder {
|
||||
b.offset = 0
|
||||
|
||||
var textLen int
|
||||
var entitiesLen int
|
||||
for _, e := range b.entries {
|
||||
textLen += len(e.text) // bytes, для Grow нормально
|
||||
entitiesLen += len(e.entities)
|
||||
}
|
||||
|
||||
b.entities = make(extypes.Slice[tgapi.MessageEntity], 0, entitiesLen)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(textLen)
|
||||
|
||||
for _, e := range b.entries {
|
||||
sb.WriteString(e.text)
|
||||
|
||||
for _, entity := range e.entities {
|
||||
entity.Offset += b.offset
|
||||
b.entities = append(b.entities, entity)
|
||||
}
|
||||
|
||||
b.offset += e.length
|
||||
}
|
||||
|
||||
b.str = sb.String()
|
||||
b.isDirty = false
|
||||
return b
|
||||
}
|
||||
func (b *MessageBuilder) markDirty() {
|
||||
b.isDirty = true
|
||||
}
|
||||
|
||||
type MessageBuilderEntry struct {
|
||||
text string
|
||||
length int
|
||||
|
||||
b *MessageBuilder
|
||||
entities extypes.Slice[tgapi.MessageEntity]
|
||||
}
|
||||
|
||||
// Add appends plain text to the message and returns its entry for formatting.
|
||||
func (b *MessageBuilder) Add(text string) *MessageBuilderEntry {
|
||||
e := &MessageBuilderEntry{
|
||||
b: b,
|
||||
entities: make(extypes.Slice[tgapi.MessageEntity], 0),
|
||||
|
||||
text: text,
|
||||
length: telegramTextLen(text),
|
||||
}
|
||||
b.entries = b.entries.Push(e)
|
||||
b.markDirty()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityMention,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityHashtag,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCashtag,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBotCommand,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityEmail,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPhoneNumber,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityItalic,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityUnderline,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityStrike,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntitySpoiler,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBlockquote,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityExpandableBlockquote,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCode,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) CodeBlockWithLanguage(lang string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
Offset: 0, Length: e.length, Language: lang,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityTextLink,
|
||||
Offset: 0, Length: e.length, URL: url,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) TextMention(user *tgapi.User) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityTextMention,
|
||||
Offset: 0, Length: e.length, User: user,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCustomEmoji,
|
||||
Offset: 0, Length: e.length, CustomEmojiID: emojiID,
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) DateTime(time time.Time) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityDateTime,
|
||||
Offset: 0, Length: e.length, UnixTime: time.Unix(),
|
||||
})
|
||||
return e
|
||||
}
|
||||
func (e *MessageBuilderEntry) DateTimeFormat(time time.Time, format string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityDateTime,
|
||||
Offset: 0, Length: e.length,
|
||||
UnixTime: time.Unix(), DateTimeFormat: format,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func telegramTextLen(text string) int {
|
||||
n := 0
|
||||
for _, r := range text {
|
||||
if r <= 0xFFFF {
|
||||
n++
|
||||
} else {
|
||||
n += 2
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (e *MessageBuilderEntry) addEntity(entity tgapi.MessageEntity) {
|
||||
if entity.Length <= 0 {
|
||||
return
|
||||
}
|
||||
e.entities = append(e.entities, entity)
|
||||
if e.b != nil {
|
||||
e.b.markDirty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package tgmd
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
func TestMessageBuilder_BuildPlainText(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("Hello")
|
||||
b.Add(", ")
|
||||
b.Add("world")
|
||||
|
||||
text, entities := b.Build()
|
||||
|
||||
if text != "Hello, world" {
|
||||
t.Fatalf("text = %q, want %q", text, "Hello, world")
|
||||
}
|
||||
|
||||
if len(entities) != 0 {
|
||||
t.Fatalf("entities len = %d, want 0", len(entities))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_EntityOffsetsAreUTF16(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("Hi ")
|
||||
b.Add("👋") // 2 UTF-16 code units
|
||||
b.Add(" ")
|
||||
b.Add("world").Bold()
|
||||
|
||||
text, entities := b.Build()
|
||||
|
||||
if text != "Hi 👋 world" {
|
||||
t.Fatalf("text = %q, want %q", text, "Hi 👋 world")
|
||||
}
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 6, // H i space = 3, 👋 = 2, space = 1
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_EntityLengthIsUTF16(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("👋").Bold()
|
||||
|
||||
text, entities := b.Build()
|
||||
|
||||
if text != "👋" {
|
||||
t.Fatalf("text = %q, want %q", text, "👋")
|
||||
}
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0,
|
||||
Length: 2,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_MultipleEntitiesOnSameEntry(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("hello").Bold().Italic()
|
||||
|
||||
_, entities := b.Build()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
{
|
||||
Type: tgapi.MessageEntityItalic,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_DoesNotDuplicateAfterRepeatedReads(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("hello").Bold()
|
||||
|
||||
text1 := b.String()
|
||||
entities1 := b.Entities()
|
||||
|
||||
text2 := b.String()
|
||||
entities2 := b.Entities()
|
||||
|
||||
if text1 != text2 {
|
||||
t.Fatalf("texts differ: %q != %q", text1, text2)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities1, entities2) {
|
||||
t.Fatalf("entities differ: %#v != %#v", entities1, entities2)
|
||||
}
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities2, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities2, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_AddEntityAfterStringMarksDirty(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
entry := b.Add("hello")
|
||||
|
||||
if got := b.String(); got != "hello" {
|
||||
t.Fatalf("String() = %q, want %q", got, "hello")
|
||||
}
|
||||
|
||||
entry.Bold()
|
||||
|
||||
entities := b.Entities()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_EntitiesReturnsCopy(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("hello").Bold()
|
||||
|
||||
entities1 := b.Entities()
|
||||
entities1[0].Offset = 999
|
||||
|
||||
entities2 := b.Entities()
|
||||
|
||||
if entities2[0].Offset != 0 {
|
||||
t.Fatalf("Entities() did not return copy: offset = %d, want 0", entities2[0].Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_BuildReturnsEntitiesCopy(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("hello").Bold()
|
||||
|
||||
_, entities1 := b.Build()
|
||||
entities1[0].Offset = 999
|
||||
|
||||
_, entities2 := b.Build()
|
||||
|
||||
if entities2[0].Offset != 0 {
|
||||
t.Fatalf("Build() did not return entities copy: offset = %d, want 0", entities2[0].Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_Reset(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("hello").Bold()
|
||||
|
||||
if got := b.String(); got != "hello" {
|
||||
t.Fatalf("String() before Reset = %q, want %q", got, "hello")
|
||||
}
|
||||
|
||||
b.Reset()
|
||||
|
||||
text, entities := b.Build()
|
||||
|
||||
if text != "" {
|
||||
t.Fatalf("text after Reset = %q, want empty", text)
|
||||
}
|
||||
|
||||
if len(entities) != 0 {
|
||||
t.Fatalf("entities len after Reset = %d, want 0", len(entities))
|
||||
}
|
||||
|
||||
b.Add("world").Italic()
|
||||
|
||||
text, entities = b.Build()
|
||||
|
||||
if text != "world" {
|
||||
t.Fatalf("text after reuse = %q, want %q", text, "world")
|
||||
}
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityItalic,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities after reuse = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_EmptyEntryDoesNotCreateEntity(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("").Bold()
|
||||
b.Add("x")
|
||||
|
||||
text, entities := b.Build()
|
||||
|
||||
if text != "x" {
|
||||
t.Fatalf("text = %q, want %q", text, "x")
|
||||
}
|
||||
|
||||
if len(entities) != 0 {
|
||||
t.Fatalf("entities len = %d, want 0: %#v", len(entities), entities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_Link(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("OpenAI").Link("https://openai.com")
|
||||
|
||||
_, entities := b.Build()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityTextLink,
|
||||
Offset: 0,
|
||||
Length: 6,
|
||||
URL: "https://openai.com",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_CodeBlockWithLanguage(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
b.Add("fmt.Println(\"hi\")").CodeBlockWithLanguage("go")
|
||||
|
||||
_, entities := b.Build()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
Offset: 0,
|
||||
Length: 17,
|
||||
Language: "go",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_DateTimeFormat(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
ts := time.Unix(1772323200, 0)
|
||||
|
||||
b.Add("date").DateTimeFormat(ts, "MMMM d, yyyy")
|
||||
|
||||
_, entities := b.Build()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tgapi.MessageEntityDateTime,
|
||||
Offset: 0,
|
||||
Length: 4,
|
||||
UnixTime: 1772323200,
|
||||
DateTimeFormat: "MMMM d, yyyy",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramTextLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "ascii",
|
||||
text: "hello",
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "cyrillic",
|
||||
text: "привет",
|
||||
want: 6,
|
||||
},
|
||||
{
|
||||
name: "emoji",
|
||||
text: "👋",
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
text: "a👋b",
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "zwj sequence",
|
||||
text: "👨👩👧👦",
|
||||
want: 11,
|
||||
},
|
||||
{
|
||||
name: "flag",
|
||||
text: "🇫🇮",
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "variation selector",
|
||||
text: "❤️",
|
||||
want: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := telegramTextLen(tt.text)
|
||||
if got != tt.want {
|
||||
t.Fatalf("telegramTextLen(%q) = %d, want %d", tt.text, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_SimpleEntityTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
add func(*MessageBuilderEntry)
|
||||
want tgapi.MessageEntityType
|
||||
}{
|
||||
{"mention", func(e *MessageBuilderEntry) { e.Mention() }, tgapi.MessageEntityMention},
|
||||
{"hashtag", func(e *MessageBuilderEntry) { e.Hashtag() }, tgapi.MessageEntityHashtag},
|
||||
{"cashtag", func(e *MessageBuilderEntry) { e.Cashtag() }, tgapi.MessageEntityCashtag},
|
||||
{"bot command", func(e *MessageBuilderEntry) { e.BotCommand() }, tgapi.MessageEntityBotCommand},
|
||||
{"email", func(e *MessageBuilderEntry) { e.Email() }, tgapi.MessageEntityEmail},
|
||||
{"phone", func(e *MessageBuilderEntry) { e.Phone() }, tgapi.MessageEntityPhoneNumber},
|
||||
{"bold", func(e *MessageBuilderEntry) { e.Bold() }, tgapi.MessageEntityBold},
|
||||
{"italic", func(e *MessageBuilderEntry) { e.Italic() }, tgapi.MessageEntityItalic},
|
||||
{"underline", func(e *MessageBuilderEntry) { e.Underline() }, tgapi.MessageEntityUnderline},
|
||||
{"strikethrough", func(e *MessageBuilderEntry) { e.Strikethrough() }, tgapi.MessageEntityStrike},
|
||||
{"spoiler", func(e *MessageBuilderEntry) { e.Spoiler() }, tgapi.MessageEntitySpoiler},
|
||||
{"quote", func(e *MessageBuilderEntry) { e.Quote() }, tgapi.MessageEntityBlockquote},
|
||||
{"expandable quote", func(e *MessageBuilderEntry) { e.ExpandableQuote() }, tgapi.MessageEntityExpandableBlockquote},
|
||||
{"inline code", func(e *MessageBuilderEntry) { e.InlineCode() }, tgapi.MessageEntityCode},
|
||||
{"code block", func(e *MessageBuilderEntry) { e.CodeBlock() }, tgapi.MessageEntityPre},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := NewMessageBuilder()
|
||||
|
||||
e := b.Add("hello")
|
||||
tt.add(e)
|
||||
|
||||
_, entities := b.Build()
|
||||
|
||||
want := []tgapi.MessageEntity{
|
||||
{
|
||||
Type: tt.want,
|
||||
Offset: 0,
|
||||
Length: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(entities, want) {
|
||||
t.Fatalf("entities = %#v, want %#v", entities, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user