move gotd fork into repo. (#111)
- update to latest telegram layer - remove some references to fields in tg.Entities that don't exist in the schema - originally added here: https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e - referenced here - https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3 - https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
// Package entity contains message formatting and styling helpers.
|
||||
package entity
|
||||
@@ -0,0 +1,150 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// SortEntities sorts entities as TDLib does it.
|
||||
func SortEntities(entity []tg.MessageEntityClass) {
|
||||
sort.Sort(entitySorter(entity))
|
||||
}
|
||||
|
||||
type entitySorter []tg.MessageEntityClass
|
||||
|
||||
func (e entitySorter) Len() int {
|
||||
return len(e)
|
||||
}
|
||||
|
||||
func (e entitySorter) Less(i, j int) bool {
|
||||
a, b := e[i], e[j]
|
||||
return a.GetOffset() < b.GetOffset() ||
|
||||
a.GetLength() > b.GetLength()
|
||||
}
|
||||
|
||||
func (e entitySorter) Swap(i, j int) {
|
||||
e[i], e[j] = e[j], e[i]
|
||||
}
|
||||
|
||||
// setLength sets Length field of entity.
|
||||
func setLength(index, value int, slice []tg.MessageEntityClass) {
|
||||
reflect.ValueOf(&slice[index]).
|
||||
Elem().Elem().Elem().
|
||||
FieldByName("Length").
|
||||
SetInt(int64(value))
|
||||
}
|
||||
|
||||
// fixEntities trims space, if needed and fixes entities offsets.
|
||||
func (b *Builder) fixEntities(msg string, entities []tg.MessageEntityClass) (string, []tg.MessageEntityClass) {
|
||||
// If there are no entities or last text block does not have entities,
|
||||
// so we just return built message.
|
||||
if len(b.lengths) == 0 || b.lastFormatIndex >= len(entities) {
|
||||
return msg, entities
|
||||
}
|
||||
|
||||
// Since Telegram client does not handle space after formatted message
|
||||
// we should compute length of the last block to trim it.
|
||||
// Get first entity of last text block.
|
||||
entity := b.lengths[len(b.lengths)-1]
|
||||
offset := entity.offset
|
||||
length := entity.length
|
||||
// Get last text block.
|
||||
lastBlock := msg[offset:]
|
||||
// Trim this block.
|
||||
trimmed := strings.TrimRightFunc(lastBlock, unicode.IsSpace)
|
||||
|
||||
// If there are a difference, we should change length of the all entities.
|
||||
if length >= len(lastBlock) && len(trimmed) != len(lastBlock) {
|
||||
length := ComputeLength(trimmed)
|
||||
for idx := range entities[b.lastFormatIndex:] {
|
||||
setLength(idx, length, entities[b.lastFormatIndex:])
|
||||
}
|
||||
|
||||
msg = msg[:offset+len(trimmed)]
|
||||
}
|
||||
|
||||
return msg, entities
|
||||
}
|
||||
|
||||
// Raw returns raw result and resets builder without fixing spaces.
|
||||
func (b *Builder) Raw() (string, []tg.MessageEntityClass) {
|
||||
msg := b.message.String()
|
||||
entities := b.entities
|
||||
b.Reset()
|
||||
return msg, entities
|
||||
}
|
||||
|
||||
// Complete returns build result and resets builder.
|
||||
func (b *Builder) Complete() (string, []tg.MessageEntityClass) {
|
||||
msg, entities := b.Raw()
|
||||
defer SortEntities(entities)
|
||||
|
||||
return b.fixEntities(msg, entities)
|
||||
}
|
||||
|
||||
// ShrinkPreCode merges following <pre> and <code> entities, if needed.
|
||||
//
|
||||
// This function is used by formatters to be compliant with TDLib.
|
||||
func (b *Builder) ShrinkPreCode() {
|
||||
b.entities = shrinkPreCode(b.entities)
|
||||
}
|
||||
|
||||
// equalRange compares ranges of given entities.
|
||||
func equalRange(a, b tg.MessageEntityClass) bool {
|
||||
return a.GetLength() == b.GetLength() && a.GetOffset() == b.GetOffset()
|
||||
}
|
||||
|
||||
// shrinkPreCode merges following <pre> and <code> entities, if needed.
|
||||
func shrinkPreCode(entities []tg.MessageEntityClass) []tg.MessageEntityClass {
|
||||
for i, j := 0, len(entities)-1; i < j; i, j = i+1, j-1 {
|
||||
entities[i], entities[j] = entities[j], entities[i]
|
||||
}
|
||||
|
||||
filter := func(keep func(prev, cur tg.MessageEntityClass) bool) []tg.MessageEntityClass {
|
||||
n := 0
|
||||
for i, val := range entities {
|
||||
if i == 0 || keep(entities[i-1], val) {
|
||||
entities[n] = val
|
||||
n++
|
||||
}
|
||||
}
|
||||
return entities[:n]
|
||||
}
|
||||
|
||||
isPreCode := func(class tg.MessageEntityClass) bool {
|
||||
typeID := class.TypeID()
|
||||
return typeID == tg.MessageEntityCodeTypeID || typeID == tg.MessageEntityPreTypeID
|
||||
}
|
||||
|
||||
hasLang := func(class tg.MessageEntityClass) bool {
|
||||
pre, ok := class.(*tg.MessageEntityPre)
|
||||
return ok && pre.Language != ""
|
||||
}
|
||||
|
||||
resetLang := func(class tg.MessageEntityClass) {
|
||||
pre, ok := class.(*tg.MessageEntityPre)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pre.Language = ""
|
||||
}
|
||||
|
||||
return filter(func(prev, cur tg.MessageEntityClass) bool {
|
||||
if !isPreCode(prev) ||
|
||||
!isPreCode(cur) ||
|
||||
prev.TypeID() == cur.TypeID() {
|
||||
// Keep if not is Pre/Code entities or if they are same.
|
||||
return true
|
||||
}
|
||||
if !equalRange(prev, cur) {
|
||||
resetLang(prev)
|
||||
resetLang(cur)
|
||||
return true
|
||||
}
|
||||
return !hasLang(prev)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func TestEnsureTrim(t *testing.T) {
|
||||
a := require.New(t)
|
||||
|
||||
prefix := "pre"
|
||||
expected := "abc\nabc"
|
||||
b := Builder{}
|
||||
b.Plain(prefix)
|
||||
b.Format(expected+"\n\n\n", Bold(), Italic())
|
||||
|
||||
msg, ent := b.Complete()
|
||||
a.Equal(prefix+expected, msg)
|
||||
a.Equal([]tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: len(prefix),
|
||||
Length: ComputeLength(expected),
|
||||
},
|
||||
&tg.MessageEntityItalic{
|
||||
Offset: len(prefix),
|
||||
Length: ComputeLength(expected),
|
||||
},
|
||||
}, ent)
|
||||
}
|
||||
|
||||
func TestComplete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format func(e *Builder)
|
||||
msg string
|
||||
entities []tg.MessageEntityClass
|
||||
}{
|
||||
{"PlainBold", func(e *Builder) {
|
||||
e.Plain("plain").Bold("bold")
|
||||
}, "plainbold", []tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: ComputeLength("plain"),
|
||||
Length: ComputeLength("bold"),
|
||||
},
|
||||
}},
|
||||
{"PlainBoldAndStrike", func(e *Builder) {
|
||||
e.Plain("plain").Format("10\n\n\n\n", Bold(), Strike())
|
||||
}, "plain10", []tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: ComputeLength("plain"),
|
||||
Length: ComputeLength("10"),
|
||||
},
|
||||
&tg.MessageEntityStrike{
|
||||
Offset: ComputeLength("plain"),
|
||||
Length: ComputeLength("10"),
|
||||
},
|
||||
}},
|
||||
{"BoldPlainBold", func(e *Builder) {
|
||||
e.Bold("bold").Plain("plain").Bold("bold2\n\n\n\n")
|
||||
}, "boldplainbold2", []tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: ComputeLength("boldplain"),
|
||||
Length: ComputeLength("bold2"),
|
||||
},
|
||||
&tg.MessageEntityBold{
|
||||
Offset: 0,
|
||||
Length: ComputeLength("bold"),
|
||||
},
|
||||
}},
|
||||
{"BoldBold", func(e *Builder) {
|
||||
e.Bold("bold\n\n\n\n").Bold("bold2\n\n\n\n")
|
||||
}, "bold\n\n\n\nbold2", []tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: 0,
|
||||
Length: ComputeLength("bold\n\n\n\n"),
|
||||
},
|
||||
&tg.MessageEntityBold{
|
||||
Offset: ComputeLength("bold\n\n\n\n"),
|
||||
Length: ComputeLength("bold2"),
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
b := Builder{}
|
||||
test.format(&b)
|
||||
|
||||
msg, entities := b.Complete()
|
||||
a.Equal(test.msg, msg)
|
||||
a.Equal(test.entities, entities)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
type utf8entity struct {
|
||||
offset int
|
||||
length int
|
||||
}
|
||||
|
||||
// Builder builds message string and text entities.
|
||||
type Builder struct {
|
||||
entities []tg.MessageEntityClass
|
||||
// lengths stores offset/length of entities too, but in UTF-8 codepoints.
|
||||
lengths []utf8entity
|
||||
// We store index of first entity added at last Format call.
|
||||
// It needed to trim space in all entities of last text block.
|
||||
lastFormatIndex int
|
||||
// utf16length stores length in UTF-16 codepoints.
|
||||
utf16length int
|
||||
// message is message string builder.
|
||||
message strings.Builder
|
||||
}
|
||||
|
||||
// GrowText grows internal buffer capacity.
|
||||
func (b *Builder) GrowText(n int) {
|
||||
b.message.Grow(n)
|
||||
}
|
||||
|
||||
// GrowEntities grows internal buffer capacity.
|
||||
func (b *Builder) GrowEntities(n int) {
|
||||
if n < 0 {
|
||||
panic("entity.Builder.GrowEntities: negative count")
|
||||
}
|
||||
|
||||
buf := make([]tg.MessageEntityClass, len(b.entities), 2*cap(b.entities)+n)
|
||||
copy(buf, b.entities)
|
||||
b.entities = buf
|
||||
}
|
||||
|
||||
// Reset resets the Builder to be empty.
|
||||
func (b *Builder) Reset() {
|
||||
b.message.Reset()
|
||||
b.entities = nil
|
||||
b.utf16length = 0
|
||||
}
|
||||
|
||||
// UTF8Len returns length of text in bytes.
|
||||
func (b *Builder) UTF8Len() int {
|
||||
return b.message.Len()
|
||||
}
|
||||
|
||||
// UTF16Len returns length of text in UTF-16 codepoints.
|
||||
func (b *Builder) UTF16Len() int {
|
||||
return b.utf16length
|
||||
}
|
||||
|
||||
// EntitiesLen return length of added entities.
|
||||
func (b *Builder) EntitiesLen() int {
|
||||
return len(b.entities)
|
||||
}
|
||||
|
||||
// TextRange returns message text of given byte (UTF-8) range.
|
||||
//
|
||||
// If range is invalid, it will panic.
|
||||
func (b *Builder) TextRange(from, to int) string {
|
||||
return b.message.String()[from:to]
|
||||
}
|
||||
|
||||
// LastEntity returns last entity if any.
|
||||
func (b *Builder) LastEntity() (tg.MessageEntityClass, bool) {
|
||||
l := b.EntitiesLen()
|
||||
if l < 1 {
|
||||
return nil, false
|
||||
}
|
||||
return b.entities[l-1], true
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func TestBuilder_TextRange(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b Builder
|
||||
)
|
||||
_, _ = b.WriteString("abc")
|
||||
a.Equal("abc"[1:2], b.TextRange(1, 2))
|
||||
a.Equal("abc"[0:0], b.TextRange(0, 0))
|
||||
|
||||
panicRanges := [][2]int{
|
||||
{1, 0},
|
||||
{-1, 0},
|
||||
{0, -1},
|
||||
}
|
||||
for _, r := range panicRanges {
|
||||
a.Panics(func() {
|
||||
b.TextRange(r[0], r[1])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_LastEntity(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b Builder
|
||||
)
|
||||
|
||||
e, ok := b.LastEntity()
|
||||
a.False(ok)
|
||||
a.Nil(e)
|
||||
b.Underline("abc")
|
||||
e, ok = b.LastEntity()
|
||||
a.True(ok)
|
||||
a.Equal(&tg.MessageEntityUnderline{
|
||||
Offset: 0,
|
||||
Length: 3,
|
||||
}, e)
|
||||
}
|
||||
|
||||
func TestBuilder_GrowText(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b Builder
|
||||
)
|
||||
|
||||
b.GrowText(100)
|
||||
a.LessOrEqual(100, b.message.Cap())
|
||||
}
|
||||
|
||||
func TestBuilder_GrowEntities(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b Builder
|
||||
)
|
||||
|
||||
b.GrowEntities(100)
|
||||
a.Equal(100, cap(b.entities))
|
||||
a.Panics(func() {
|
||||
b.GrowEntities(-1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// Code generated by mkentity, DO NOT EDIT.
|
||||
package entity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
var (
|
||||
_ = tg.Invoker(nil)
|
||||
_ = context.Context(nil)
|
||||
)
|
||||
|
||||
// Unknown creates Formatter of Unknown message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUnknown.
|
||||
func Unknown() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityUnknown{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown adds and formats message as Unknown message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUnknown.
|
||||
func (b *Builder) Unknown(s string) *Builder {
|
||||
return b.Format(s, Unknown())
|
||||
}
|
||||
|
||||
// Mention creates Formatter of Mention message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityMention.
|
||||
func Mention() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityMention{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mention adds and formats message as Mention message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityMention.
|
||||
func (b *Builder) Mention(s string) *Builder {
|
||||
return b.Format(s, Mention())
|
||||
}
|
||||
|
||||
// Hashtag creates Formatter of Hashtag message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityHashtag.
|
||||
func Hashtag() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityHashtag{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hashtag adds and formats message as Hashtag message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityHashtag.
|
||||
func (b *Builder) Hashtag(s string) *Builder {
|
||||
return b.Format(s, Hashtag())
|
||||
}
|
||||
|
||||
// BotCommand creates Formatter of BotCommand message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBotCommand.
|
||||
func BotCommand() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityBotCommand{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BotCommand adds and formats message as BotCommand message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBotCommand.
|
||||
func (b *Builder) BotCommand(s string) *Builder {
|
||||
return b.Format(s, BotCommand())
|
||||
}
|
||||
|
||||
// URL creates Formatter of URL message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUrl.
|
||||
func URL() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityURL{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URL adds and formats message as URL message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUrl.
|
||||
func (b *Builder) URL(s string) *Builder {
|
||||
return b.Format(s, URL())
|
||||
}
|
||||
|
||||
// Email creates Formatter of Email message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityEmail.
|
||||
func Email() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityEmail{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Email adds and formats message as Email message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityEmail.
|
||||
func (b *Builder) Email(s string) *Builder {
|
||||
return b.Format(s, Email())
|
||||
}
|
||||
|
||||
// Bold creates Formatter of Bold message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBold.
|
||||
func Bold() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityBold{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bold adds and formats message as Bold message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBold.
|
||||
func (b *Builder) Bold(s string) *Builder {
|
||||
return b.Format(s, Bold())
|
||||
}
|
||||
|
||||
// Italic creates Formatter of Italic message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityItalic.
|
||||
func Italic() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityItalic{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Italic adds and formats message as Italic message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityItalic.
|
||||
func (b *Builder) Italic(s string) *Builder {
|
||||
return b.Format(s, Italic())
|
||||
}
|
||||
|
||||
// Code creates Formatter of Code message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCode.
|
||||
func Code() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityCode{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Code adds and formats message as Code message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCode.
|
||||
func (b *Builder) Code(s string) *Builder {
|
||||
return b.Format(s, Code())
|
||||
}
|
||||
|
||||
// Pre creates Formatter of Pre message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityPre.
|
||||
func Pre(language string) Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityPre{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
Language: language,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre adds and formats message as Pre message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityPre.
|
||||
func (b *Builder) Pre(s string, language string) *Builder {
|
||||
return b.Format(s, Pre(language))
|
||||
}
|
||||
|
||||
// TextURL creates Formatter of TextURL message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityTextUrl.
|
||||
func TextURL(uRL string) Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityTextURL{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
URL: uRL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TextURL adds and formats message as TextURL message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityTextUrl.
|
||||
func (b *Builder) TextURL(s string, uRL string) *Builder {
|
||||
return b.Format(s, TextURL(uRL))
|
||||
}
|
||||
|
||||
// MentionName creates Formatter of MentionName message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/inputMessageEntityMentionName.
|
||||
func MentionName(userID tg.InputUserClass) Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.InputMessageEntityMentionName{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
UserID: userID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MentionName adds and formats message as MentionName message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/inputMessageEntityMentionName.
|
||||
func (b *Builder) MentionName(s string, userID tg.InputUserClass) *Builder {
|
||||
return b.Format(s, MentionName(userID))
|
||||
}
|
||||
|
||||
// Phone creates Formatter of Phone message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityPhone.
|
||||
func Phone() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityPhone{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phone adds and formats message as Phone message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityPhone.
|
||||
func (b *Builder) Phone(s string) *Builder {
|
||||
return b.Format(s, Phone())
|
||||
}
|
||||
|
||||
// Cashtag creates Formatter of Cashtag message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCashtag.
|
||||
func Cashtag() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityCashtag{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cashtag adds and formats message as Cashtag message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCashtag.
|
||||
func (b *Builder) Cashtag(s string) *Builder {
|
||||
return b.Format(s, Cashtag())
|
||||
}
|
||||
|
||||
// Underline creates Formatter of Underline message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUnderline.
|
||||
func Underline() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityUnderline{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Underline adds and formats message as Underline message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityUnderline.
|
||||
func (b *Builder) Underline(s string) *Builder {
|
||||
return b.Format(s, Underline())
|
||||
}
|
||||
|
||||
// Strike creates Formatter of Strike message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityStrike.
|
||||
func Strike() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityStrike{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strike adds and formats message as Strike message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityStrike.
|
||||
func (b *Builder) Strike(s string) *Builder {
|
||||
return b.Format(s, Strike())
|
||||
}
|
||||
|
||||
// BankCard creates Formatter of BankCard message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBankCard.
|
||||
func BankCard() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityBankCard{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BankCard adds and formats message as BankCard message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBankCard.
|
||||
func (b *Builder) BankCard(s string) *Builder {
|
||||
return b.Format(s, BankCard())
|
||||
}
|
||||
|
||||
// Spoiler creates Formatter of Spoiler message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntitySpoiler.
|
||||
func Spoiler() Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntitySpoiler{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spoiler adds and formats message as Spoiler message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntitySpoiler.
|
||||
func (b *Builder) Spoiler(s string) *Builder {
|
||||
return b.Format(s, Spoiler())
|
||||
}
|
||||
|
||||
// CustomEmoji creates Formatter of CustomEmoji message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCustomEmoji.
|
||||
func CustomEmoji(documentID int64) Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityCustomEmoji{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
DocumentID: documentID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CustomEmoji adds and formats message as CustomEmoji message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityCustomEmoji.
|
||||
func (b *Builder) CustomEmoji(s string, documentID int64) *Builder {
|
||||
return b.Format(s, CustomEmoji(documentID))
|
||||
}
|
||||
|
||||
// Blockquote creates Formatter of Blockquote message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBlockquote.
|
||||
func Blockquote(collapsed bool) Formatter {
|
||||
return func(offset, length int) tg.MessageEntityClass {
|
||||
return &tg.MessageEntityBlockquote{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
Collapsed: collapsed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blockquote adds and formats message as Blockquote message entity.
|
||||
//
|
||||
// See https://core.telegram.org/constructor/messageEntityBlockquote.
|
||||
func (b *Builder) Blockquote(s string, collapsed bool) *Builder {
|
||||
return b.Format(s, Blockquote(collapsed))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package entity
|
||||
|
||||
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
|
||||
// Formatter is a message entity constructor.
|
||||
type Formatter func(offset, limit int) tg.MessageEntityClass
|
||||
|
||||
// Plain formats message as plain text.
|
||||
func (b *Builder) Plain(s string) *Builder {
|
||||
_, _ = b.WriteString(s)
|
||||
b.lastFormatIndex = len(b.entities)
|
||||
return b
|
||||
}
|
||||
|
||||
// Format formats message using given formatters.
|
||||
func (b *Builder) Format(s string, formats ...Formatter) *Builder {
|
||||
return b.appendMessage(s, formats...)
|
||||
}
|
||||
|
||||
//go:generate go run go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message/internal/mkentity -output options.gen.go
|
||||
@@ -0,0 +1,186 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func TestBuilder(t *testing.T) {
|
||||
b := Builder{}
|
||||
t.Run("Plain", func(t *testing.T) {
|
||||
_, ent := b.Plain("abc").Complete()
|
||||
require.Empty(t, ent)
|
||||
})
|
||||
t.Run("EmptyString", func(t *testing.T) {
|
||||
msg, ent := b.Bold("").Complete()
|
||||
require.Empty(t, msg)
|
||||
require.Empty(t, ent)
|
||||
})
|
||||
t.Run("Format", func(t *testing.T) {
|
||||
_, ent := b.Format("abc", Bold(), Italic()).Complete()
|
||||
require.Equal(t, []tg.MessageEntityClass{
|
||||
&tg.MessageEntityBold{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
},
|
||||
&tg.MessageEntityItalic{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
},
|
||||
}, ent)
|
||||
})
|
||||
t.Run("Unknown", func(t *testing.T) {
|
||||
_, ent := b.Unknown("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityUnknown{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Mention", func(t *testing.T) {
|
||||
_, ent := b.Mention("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityMention{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Hashtag", func(t *testing.T) {
|
||||
_, ent := b.Hashtag("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityHashtag{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("BotCommand", func(t *testing.T) {
|
||||
_, ent := b.BotCommand("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityBotCommand{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("URL", func(t *testing.T) {
|
||||
_, ent := b.URL("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityURL{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Email", func(t *testing.T) {
|
||||
_, ent := b.Email("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityEmail{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Bold", func(t *testing.T) {
|
||||
_, ent := b.Bold("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityBold{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Italic", func(t *testing.T) {
|
||||
_, ent := b.Italic("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityItalic{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Code", func(t *testing.T) {
|
||||
_, ent := b.Code("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityCode{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Pre", func(t *testing.T) {
|
||||
_, ent := b.Pre("abc", "lang").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityPre{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
Language: "lang",
|
||||
}, r)
|
||||
})
|
||||
t.Run("TextURL", func(t *testing.T) {
|
||||
_, ent := b.TextURL("abc", "url").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityTextURL{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
URL: "url",
|
||||
}, r)
|
||||
})
|
||||
t.Run("MentionName", func(t *testing.T) {
|
||||
user := &tg.InputUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
}
|
||||
_, ent := b.MentionName("abc", user).Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.InputMessageEntityMentionName{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
UserID: user,
|
||||
}, r)
|
||||
})
|
||||
t.Run("Phone", func(t *testing.T) {
|
||||
_, ent := b.Phone("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityPhone{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Cashtag", func(t *testing.T) {
|
||||
_, ent := b.Cashtag("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityCashtag{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Underline", func(t *testing.T) {
|
||||
_, ent := b.Underline("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityUnderline{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Strike", func(t *testing.T) {
|
||||
_, ent := b.Strike("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityStrike{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("Blockquote", func(t *testing.T) {
|
||||
_, ent := b.Blockquote("abc", false).Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityBlockquote{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
t.Run("BankCard", func(t *testing.T) {
|
||||
_, ent := b.BankCard("abc").Complete()
|
||||
r := ent[0]
|
||||
require.Equal(t, &tg.MessageEntityBankCard{
|
||||
Offset: 0,
|
||||
Length: len("abc"),
|
||||
}, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package entity
|
||||
|
||||
// Token represents raw point in a message string.
|
||||
type Token struct {
|
||||
utf8offset int
|
||||
utf16offset int
|
||||
}
|
||||
|
||||
// UTF8Offset return UTF-8 offset.
|
||||
func (t Token) UTF8Offset() int {
|
||||
return t.utf8offset
|
||||
}
|
||||
|
||||
// UTF16Offset returns UTF-16 offset.
|
||||
func (t Token) UTF16Offset() int {
|
||||
return t.utf16offset
|
||||
}
|
||||
|
||||
// UTF8Length return UTF-8 length between token start and current state.
|
||||
func (t Token) UTF8Length(builder *Builder) int {
|
||||
return builder.UTF8Len() - t.utf8offset
|
||||
}
|
||||
|
||||
// UTF16Length returns UTF-16 length between token start and current state.
|
||||
func (t Token) UTF16Length(builder *Builder) int {
|
||||
return builder.UTF16Len() - t.utf16offset
|
||||
}
|
||||
|
||||
// Text message string between token start and current state.
|
||||
func (t Token) Text(builder *Builder) string {
|
||||
return builder.TextRange(t.utf8offset, builder.UTF8Len())
|
||||
}
|
||||
|
||||
// Apply formats range between token start and current state using given Formatter slice.
|
||||
func (t Token) Apply(builder *Builder, f ...Formatter) {
|
||||
builder.appendEntities(t.utf16offset, t.UTF16Length(builder), utf8entity{
|
||||
offset: t.utf8offset,
|
||||
length: t.UTF8Length(builder),
|
||||
}, f...)
|
||||
}
|
||||
|
||||
// Token creates new Token.
|
||||
func (b *Builder) Token() Token {
|
||||
return Token{
|
||||
utf8offset: b.UTF8Len(),
|
||||
utf16offset: b.UTF16Len(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func TestToken_Apply(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b = &Builder{}
|
||||
)
|
||||
_ = b.WriteByte('a')
|
||||
tok := b.Token()
|
||||
a.Equal(1, tok.UTF8Offset())
|
||||
a.Equal(1, tok.UTF16Offset())
|
||||
|
||||
a.Zero(tok.UTF8Length(b))
|
||||
a.Zero(tok.UTF16Length(b))
|
||||
a.Empty(tok.Text(b))
|
||||
|
||||
text := "abc🏳"
|
||||
_, _ = b.WriteString(text)
|
||||
a.Equal(text, tok.Text(b))
|
||||
utf16Len := ComputeLength(tok.Text(b))
|
||||
|
||||
a.Equal(b.message.Len()-tok.UTF8Offset(), tok.UTF8Length(b))
|
||||
a.Equal(utf16Len, tok.UTF16Length(b))
|
||||
|
||||
tok.Apply(b, Bold())
|
||||
a.Equal(1, b.EntitiesLen())
|
||||
e, ok := b.LastEntity()
|
||||
a.True(ok)
|
||||
a.Equal(&tg.MessageEntityBold{Offset: 1, Length: utf16Len}, e)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
|
||||
// UserResolver is callback for resolving InputUser by ID.
|
||||
type UserResolver = func(id int64) (tg.InputUserClass, error)
|
||||
@@ -0,0 +1,108 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ComputeLength returns length of s encoded as UTF-16 string.
|
||||
//
|
||||
// While Telegram API docs state that they expect the number of UTF-8
|
||||
// code points, in fact they are talking about UTF-16 code units.
|
||||
func ComputeLength(s string) int {
|
||||
// From utf16 package.
|
||||
n := 0
|
||||
for _, v := range s {
|
||||
n += utf16RuneLen(v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ComputeLengthBytes returns length of s encoded as UTF-16 string.
|
||||
//
|
||||
// While Telegram API docs state that they expect the number of UTF-8
|
||||
// code points, in fact they are talking about UTF-16 code units.
|
||||
func ComputeLengthBytes(s []byte) (n int) {
|
||||
// From utf16 package.
|
||||
var i int
|
||||
for i < len(s) {
|
||||
v, size := utf8.DecodeRune(s[i:])
|
||||
i += size
|
||||
n += utf16RuneLen(v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func utf16RuneLen(v rune) int {
|
||||
const (
|
||||
surrSelf = 0x10000
|
||||
maxRune = '\U0010FFFF' // Maximum valid Unicode code point.
|
||||
)
|
||||
|
||||
if surrSelf <= v && v <= maxRune {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (b *Builder) appendMessage(s string, formats ...Formatter) *Builder {
|
||||
if s == "" {
|
||||
return b
|
||||
}
|
||||
|
||||
offset := b.utf16length
|
||||
length := ComputeLength(s)
|
||||
|
||||
b.appendEntities(offset, length, utf8entity{
|
||||
offset: b.message.Len(),
|
||||
length: len(s),
|
||||
}, formats...)
|
||||
_, _ = b.WriteString(s)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) appendEntities(offset, length int, u utf8entity, formats ...Formatter) *Builder {
|
||||
b.lastFormatIndex = len(b.entities)
|
||||
for i := range formats {
|
||||
b.entities = append(b.entities, formats[i](offset, length))
|
||||
b.lengths = append(b.lengths, u)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var _ = []interface {
|
||||
io.Writer
|
||||
io.StringWriter
|
||||
io.ByteWriter
|
||||
WriteRune(rune) (int, error)
|
||||
}{
|
||||
(*Builder)(nil),
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (b *Builder) Write(s []byte) (int, error) {
|
||||
n, err := b.message.Write(s)
|
||||
b.utf16length += ComputeLengthBytes(s)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// WriteString implements io.StringWriter.
|
||||
func (b *Builder) WriteString(s string) (int, error) {
|
||||
n, err := b.message.WriteString(s)
|
||||
b.utf16length += ComputeLength(s)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// WriteByte implements io.ByteWriter.
|
||||
func (b *Builder) WriteByte(s byte) error {
|
||||
err := b.message.WriteByte(s)
|
||||
b.utf16length++
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteRune implements rune writer.
|
||||
func (b *Builder) WriteRune(s rune) (int, error) {
|
||||
n, err := b.message.WriteRune(s)
|
||||
b.utf16length += utf16RuneLen(s)
|
||||
return n, err
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
)
|
||||
|
||||
func TestComputeLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
want int
|
||||
}{
|
||||
{string([]rune{97, 127987, 65039, 8205, 127752}), 7},
|
||||
{string([]int32{97, 127987, 65039, 8205, 127752, 127987, 65039, 8205, 127752}), 13},
|
||||
{string([]int32{97, 128104, 8205, 128102, 8205, 128102}), 9},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
r := []byte(tt.s)
|
||||
testutil.ZeroAlloc(t, func() {
|
||||
_ = ComputeLength(tt.s)
|
||||
})
|
||||
testutil.ZeroAlloc(t, func() {
|
||||
_ = ComputeLengthBytes(r)
|
||||
})
|
||||
t.Run(hex.EncodeToString([]byte(tt.s)), func(t *testing.T) {
|
||||
require.Equal(t, tt.want, ComputeLength(tt.s))
|
||||
require.Equal(t, tt.want, ComputeLengthBytes(r))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Write(t *testing.T) {
|
||||
var (
|
||||
a = require.New(t)
|
||||
b Builder
|
||||
)
|
||||
_, err := b.Write([]byte("abc"))
|
||||
a.NoError(err)
|
||||
_, err = b.WriteString("abc")
|
||||
a.NoError(err)
|
||||
a.NoError(b.WriteByte('\n'))
|
||||
a.Equal(3+3+1, b.UTF8Len())
|
||||
a.Equal(3+3+1, b.UTF16Len())
|
||||
|
||||
var r rune = 127987
|
||||
_, err = b.WriteRune(r)
|
||||
a.NoError(err)
|
||||
a.Equal(3+3+1+utf8.RuneLen(r), b.UTF8Len())
|
||||
a.Equal(3+3+1+utf16RuneLen(r), b.UTF16Len())
|
||||
}
|
||||
Reference in New Issue
Block a user