(new): tgfmt package
(fix): formatting helpers (ci/cd): go checks (tests): tgfmt coverage
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
package tgfmt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/extypes"
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// Build returns the built message text and a copy of its entities.
|
||||
func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
|
||||
if b.isDirty {
|
||||
b.update()
|
||||
}
|
||||
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
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// MessageBuilderEntry represents text appended to a MessageBuilder.
|
||||
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
|
||||
}
|
||||
|
||||
// Mention marks the entry as a Telegram mention.
|
||||
func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityMention,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Hashtag marks the entry as a Telegram hashtag.
|
||||
func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityHashtag,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Cashtag marks the entry as a Telegram cashtag.
|
||||
func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCashtag,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// BotCommand marks the entry as a Telegram bot command.
|
||||
func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBotCommand,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Email marks the entry as an email address.
|
||||
func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityEmail,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Phone marks the entry as a phone number.
|
||||
func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPhoneNumber,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Bold marks the entry as bold text.
|
||||
func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBold,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Italic marks the entry as italic text.
|
||||
func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityItalic,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Underline marks the entry as underlined text.
|
||||
func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityUnderline,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Strikethrough marks the entry as strikethrough text.
|
||||
func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityStrike,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Spoiler marks the entry as spoiler text.
|
||||
func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntitySpoiler,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Quote marks the entry as a blockquote.
|
||||
func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityBlockquote,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// ExpandableQuote marks the entry as an expandable blockquote.
|
||||
func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityExpandableBlockquote,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// InlineCode marks the entry as inline code.
|
||||
func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCode,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// CodeBlock marks the entry as a preformatted code block.
|
||||
func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityPre,
|
||||
Offset: 0, Length: e.length,
|
||||
})
|
||||
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,
|
||||
Offset: 0, Length: e.length, Language: lang,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Link marks the entry as a text link.
|
||||
func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityTextLink,
|
||||
Offset: 0, Length: e.length, URL: url,
|
||||
})
|
||||
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,
|
||||
Offset: 0, Length: e.length, User: user,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// CustomEmoji marks the entry as a custom emoji.
|
||||
func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
|
||||
e.addEntity(tgapi.MessageEntity{
|
||||
Type: tgapi.MessageEntityCustomEmoji,
|
||||
Offset: 0, Length: e.length, CustomEmojiID: emojiID,
|
||||
})
|
||||
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,
|
||||
Offset: 0, Length: e.length, UnixTime: time.Unix(),
|
||||
})
|
||||
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,
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user