aboutsummaryrefslogblamecommitdiff
path: root/telegram/formatter/formatter.go
blob: 2946a5599ef1bdf80e46f7573adb4e05560fa8ae (plain) (tree)
1
2
3
4
5
6
7





                                        
                                            











                                                     

                                    

                                 


























































                                                                                                     
                                                                                 








                                                                                             
                                                                                      




                       






















                                                                                             

































































                                                                                    
package formatter

import (
	"sort"

	log "github.com/sirupsen/logrus"
	"github.com/zelenin/go-tdlib/client"
)

// 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

var boldRunesMarkdown = []rune("**")
var boldRunesXEP0393 = []rune("*")
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:
		return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown)
	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)
		return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")"))
	}

	return nil, nil
}

// EntityToXEP0393 generates the wrapping XEP-0393 tags
func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) {
	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
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)
}