2020-01-09 21:16:40 +00:00
|
|
|
package formatter
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
2022-01-17 20:45:40 +00:00
|
|
|
"github.com/zelenin/go-tdlib/client"
|
2020-01-09 21:16:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Insertion is a piece of text in given position
|
|
|
|
type Insertion struct {
|
|
|
|
Offset int32
|
|
|
|
Runes []rune
|
|
|
|
}
|
|
|
|
|
|
|
|
// InsertionStack contains the sequence of insertions
|
|
|
|
// from the start or from the end
|
|
|
|
type InsertionStack []*Insertion
|
|
|
|
|
2021-12-18 16:04:24 +00:00
|
|
|
var boldRunesMarkdown = []rune("**")
|
|
|
|
var boldRunesXEP0393 = []rune("*")
|
2020-01-09 21:16:40 +00:00
|
|
|
var italicRunes = []rune("_")
|
|
|
|
var codeRunes = []rune("\n```\n")
|
|
|
|
|
|
|
|
// rebalance pumps all the values at given offset to current stack (growing
|
|
|
|
// from start) from given stack (growing from end); should be called
|
|
|
|
// before any insertions to the current stack at the given offset
|
|
|
|
func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionStack, InsertionStack) {
|
|
|
|
for len(s2) > 0 && s2[len(s2)-1].Offset <= offset {
|
|
|
|
s = append(s, s2[len(s2)-1])
|
|
|
|
s2 = s2[:len(s2)-1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return s, s2
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIterator is a second order function that sequentially scans and returns
|
|
|
|
// stack elements; starts returning nil when elements are ended
|
|
|
|
func (s InsertionStack) NewIterator() func() *Insertion {
|
|
|
|
i := -1
|
|
|
|
|
|
|
|
return func() *Insertion {
|
|
|
|
i++
|
|
|
|
if i < len(s) {
|
|
|
|
return s[i]
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SortEntities arranges the entities in traversal-ready order
|
|
|
|
func SortEntities(entities []*client.TextEntity) []*client.TextEntity {
|
|
|
|
sortedEntities := make([]*client.TextEntity, len(entities))
|
|
|
|
copy(sortedEntities, entities)
|
|
|
|
|
|
|
|
sort.Slice(sortedEntities, func(i int, j int) bool {
|
|
|
|
entity1 := entities[i]
|
|
|
|
entity2 := entities[j]
|
|
|
|
if entity1.Offset < entity2.Offset {
|
|
|
|
return true
|
|
|
|
} else if entity1.Offset == entity2.Offset {
|
|
|
|
return entity1.Length > entity2.Length
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
return sortedEntities
|
|
|
|
}
|
|
|
|
|
|
|
|
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) (*Insertion, *Insertion) {
|
|
|
|
return &Insertion{
|
|
|
|
Offset: entity.Offset,
|
|
|
|
Runes: lbrace,
|
|
|
|
}, &Insertion{
|
|
|
|
Offset: entity.Offset + entity.Length,
|
|
|
|
Runes: rbrace,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// EntityToMarkdown generates the wrapping Markdown tags
|
|
|
|
func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) {
|
|
|
|
switch entity.Type.TextEntityTypeType() {
|
|
|
|
case client.TypeTextEntityTypeBold:
|
2021-12-18 16:04:24 +00:00
|
|
|
return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown)
|
2020-01-09 21:16:40 +00:00
|
|
|
case client.TypeTextEntityTypeItalic:
|
|
|
|
return markupBraces(entity, italicRunes, italicRunes)
|
|
|
|
case client.TypeTextEntityTypeCode, client.TypeTextEntityTypePre:
|
|
|
|
return markupBraces(entity, codeRunes, codeRunes)
|
|
|
|
case client.TypeTextEntityTypePreCode:
|
|
|
|
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
|
|
|
|
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
|
|
|
|
case client.TypeTextEntityTypeTextUrl:
|
|
|
|
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
|
2021-12-18 16:04:24 +00:00
|
|
|
return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")"))
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2021-12-18 16:04:24 +00:00
|
|
|
// EntityToXEP0393 generates the wrapping XEP-0393 tags
|
|
|
|
func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) {
|
2022-02-18 23:41:08 +00:00
|
|
|
if entity == nil || entity.Type == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2021-12-18 16:04:24 +00:00
|
|
|
switch entity.Type.TextEntityTypeType() {
|
|
|
|
case client.TypeTextEntityTypeBold:
|
|
|
|
return markupBraces(entity, boldRunesXEP0393, boldRunesXEP0393)
|
|
|
|
case client.TypeTextEntityTypeItalic:
|
|
|
|
return markupBraces(entity, italicRunes, italicRunes)
|
|
|
|
case client.TypeTextEntityTypeCode, client.TypeTextEntityTypePre:
|
|
|
|
return markupBraces(entity, codeRunes, codeRunes)
|
|
|
|
case client.TypeTextEntityTypePreCode:
|
|
|
|
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
|
|
|
|
// TODO: inline code support (non-standard too)
|
|
|
|
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
|
|
|
|
case client.TypeTextEntityTypeTextUrl:
|
|
|
|
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
|
|
|
|
// non-standard, Pidgin-specific
|
|
|
|
return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">"))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format traverses an already sorted list of entities and wraps the text in a markup
|
2020-01-09 21:16:40 +00:00
|
|
|
func Format(
|
|
|
|
sourceText string,
|
|
|
|
entities []*client.TextEntity,
|
|
|
|
entityToMarkup func(*client.TextEntity) (*Insertion, *Insertion),
|
|
|
|
) string {
|
|
|
|
if len(entities) == 0 {
|
|
|
|
return sourceText
|
|
|
|
}
|
|
|
|
|
|
|
|
startStack := make(InsertionStack, 0, len(sourceText))
|
|
|
|
endStack := make(InsertionStack, 0, len(sourceText))
|
|
|
|
|
|
|
|
// convert entities to a stack of brackets
|
|
|
|
var maxEndOffset int32
|
|
|
|
for _, entity := range entities {
|
|
|
|
log.Debugf("%#v", entity)
|
|
|
|
if entity.Length <= 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
endOffset := entity.Offset + entity.Length
|
|
|
|
if endOffset > maxEndOffset {
|
|
|
|
maxEndOffset = endOffset
|
|
|
|
}
|
|
|
|
|
|
|
|
startStack, endStack = startStack.rebalance(endStack, entity.Offset)
|
|
|
|
|
|
|
|
startInsertion, endInsertion := entityToMarkup(entity)
|
|
|
|
if startInsertion != nil {
|
|
|
|
startStack = append(startStack, startInsertion)
|
|
|
|
}
|
|
|
|
if endInsertion != nil {
|
|
|
|
endStack = append(endStack, endInsertion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// flush the closing brackets that still remain in endStack
|
|
|
|
startStack, endStack = startStack.rebalance(endStack, maxEndOffset)
|
|
|
|
|
|
|
|
// merge brackets into text
|
|
|
|
markupRunes := make([]rune, 0, len(sourceText))
|
|
|
|
|
|
|
|
nextInsertion := startStack.NewIterator()
|
|
|
|
insertion := nextInsertion()
|
|
|
|
var runeI int32
|
|
|
|
|
|
|
|
for _, cp := range sourceText {
|
|
|
|
for insertion != nil && insertion.Offset <= runeI {
|
|
|
|
markupRunes = append(markupRunes, insertion.Runes...)
|
|
|
|
insertion = nextInsertion()
|
|
|
|
}
|
|
|
|
|
|
|
|
markupRunes = append(markupRunes, cp)
|
|
|
|
// skip two UTF-16 code units (not points actually!) if needed
|
|
|
|
if cp > 0x0000ffff {
|
|
|
|
runeI += 2
|
|
|
|
} else {
|
|
|
|
runeI++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for insertion != nil {
|
|
|
|
markupRunes = append(markupRunes, insertion.Runes...)
|
|
|
|
insertion = nextInsertion()
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(markupRunes)
|
|
|
|
}
|