2020-01-09 21:16:40 +00:00
|
|
|
package formatter
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sort"
|
2022-03-12 01:27:15 +00:00
|
|
|
"unicode"
|
2020-01-09 21:16:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
)
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
type insertionType int
|
|
|
|
const (
|
|
|
|
insertionOpening insertionType = iota
|
|
|
|
insertionClosing
|
|
|
|
insertionUnpaired
|
|
|
|
)
|
|
|
|
|
|
|
|
type MarkupModeType int
|
|
|
|
const (
|
|
|
|
MarkupModeXEP0393 MarkupModeType = iota
|
|
|
|
MarkupModeMarkdown
|
|
|
|
)
|
|
|
|
|
|
|
|
// insertion is a piece of text in given position
|
|
|
|
type insertion struct {
|
2020-01-09 21:16:40 +00:00
|
|
|
Offset int32
|
|
|
|
Runes []rune
|
2023-11-16 00:38:45 +00:00
|
|
|
Type insertionType
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
// insertionStack contains the sequence of insertions
|
2020-01-09 21:16:40 +00:00
|
|
|
// from the start or from the end
|
2023-11-16 00:38:45 +00:00
|
|
|
type insertionStack []*insertion
|
2020-01-09 21:16:40 +00:00
|
|
|
|
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("_")
|
2022-03-11 17:54:03 +00:00
|
|
|
var strikeRunesMarkdown = []rune("~~")
|
|
|
|
var strikeRunesXEP0393 = []rune("~")
|
2022-03-11 17:01:38 +00:00
|
|
|
var codeRunes = []rune("`")
|
|
|
|
var preRuneStart = []rune("```\n")
|
|
|
|
var preRuneEnd = []rune("\n```")
|
2023-11-16 00:38:45 +00:00
|
|
|
var quoteRunes = []rune("> ")
|
|
|
|
var newlineRunes = []rune("\n")
|
|
|
|
var doubleNewlineRunes = []rune("\n\n")
|
|
|
|
var newlineCode = rune(0x0000000a)
|
|
|
|
var bmpCeil = rune(0x0000ffff)
|
2020-01-09 21:16:40 +00:00
|
|
|
|
2022-03-11 16:12:36 +00:00
|
|
|
// rebalance pumps all the values until the given offset to current stack (growing
|
2020-01-09 21:16:40 +00:00
|
|
|
// from start) from given stack (growing from end); should be called
|
|
|
|
// before any insertions to the current stack at the given offset
|
2023-11-16 00:38:45 +00:00
|
|
|
func (s insertionStack) rebalance(s2 insertionStack, offset int32) (insertionStack, insertionStack) {
|
2020-01-09 21:16:40 +00:00
|
|
|
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
|
2023-11-16 00:38:45 +00:00
|
|
|
func (s insertionStack) NewIterator() func() *insertion {
|
2020-01-09 21:16:40 +00:00
|
|
|
i := -1
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
return func() *insertion {
|
2020-01-09 21:16:40 +00:00
|
|
|
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 {
|
2022-03-13 12:55:59 +00:00
|
|
|
entity1 := sortedEntities[i]
|
|
|
|
entity2 := sortedEntities[j]
|
2020-01-09 21:16:40 +00:00
|
|
|
if entity1.Offset < entity2.Offset {
|
|
|
|
return true
|
|
|
|
} else if entity1.Offset == entity2.Offset {
|
|
|
|
return entity1.Length > entity2.Length
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
return sortedEntities
|
|
|
|
}
|
|
|
|
|
2022-03-11 16:12:36 +00:00
|
|
|
// MergeAdjacentEntities merges entities of a same kind
|
|
|
|
func MergeAdjacentEntities(entities []*client.TextEntity) []*client.TextEntity {
|
|
|
|
mergedEntities := make([]*client.TextEntity, 0, len(entities))
|
|
|
|
excludedIndices := make(map[int]bool)
|
|
|
|
|
|
|
|
for i, entity := range entities {
|
2022-03-14 20:00:00 +00:00
|
|
|
if excludedIndices[i] || entity.Type == nil {
|
2022-03-11 16:12:36 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
typ := entity.Type.TextEntityTypeType()
|
|
|
|
start := entity.Offset
|
|
|
|
end := start + entity.Length
|
|
|
|
ei := make(map[int]bool)
|
|
|
|
|
|
|
|
// collect continuations
|
|
|
|
for j, entity2 := range entities[i+1:] {
|
2022-03-14 20:00:00 +00:00
|
|
|
if entity2.Type != nil && entity2.Type.TextEntityTypeType() == typ && entity2.Offset == end {
|
2022-03-11 16:12:36 +00:00
|
|
|
end += entity2.Length
|
|
|
|
ei[j+i+1] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for intersections with other entities
|
|
|
|
var isIntersecting bool
|
|
|
|
if len(ei) > 0 {
|
|
|
|
for _, entity2 := range entities {
|
|
|
|
entity2End := entity2.Offset + entity2.Length
|
|
|
|
if (entity2.Offset < start && entity2End > start && entity2End < end) ||
|
|
|
|
(entity2.Offset > start && entity2.Offset < end && entity2End > end) {
|
|
|
|
isIntersecting = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isIntersecting {
|
|
|
|
entity.Length = end - start
|
|
|
|
for j := range ei {
|
|
|
|
excludedIndices[j] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mergedEntities = append(mergedEntities, entity)
|
|
|
|
}
|
|
|
|
|
|
|
|
return mergedEntities
|
|
|
|
}
|
|
|
|
|
2022-03-12 01:27:15 +00:00
|
|
|
// ClaspDirectives to the following span as required by XEP-0393
|
2023-11-16 00:38:45 +00:00
|
|
|
func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*client.TextEntity {
|
2022-03-12 01:27:15 +00:00
|
|
|
alignedEntities := make([]*client.TextEntity, len(entities))
|
|
|
|
copy(alignedEntities, entities)
|
|
|
|
|
|
|
|
for i, entity := range alignedEntities {
|
|
|
|
var dirty bool
|
|
|
|
endOffset := entity.Offset + entity.Length
|
|
|
|
|
|
|
|
if unicode.IsSpace(doubledRunes[entity.Offset]) {
|
2022-04-01 16:35:54 +00:00
|
|
|
for j, r := range doubledRunes[entity.Offset+1 : endOffset] {
|
2022-03-12 01:27:15 +00:00
|
|
|
if !unicode.IsSpace(r) {
|
|
|
|
dirty = true
|
2022-04-01 16:35:54 +00:00
|
|
|
entity.Offset += int32(j + 1)
|
|
|
|
entity.Length -= int32(j + 1)
|
2022-03-12 01:27:15 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if unicode.IsSpace(doubledRunes[endOffset-1]) {
|
2022-04-01 16:35:54 +00:00
|
|
|
for j := endOffset - 2; j >= entity.Offset; j-- {
|
2022-03-12 01:27:15 +00:00
|
|
|
if !unicode.IsSpace(doubledRunes[j]) {
|
|
|
|
dirty = true
|
2022-04-01 16:35:54 +00:00
|
|
|
entity.Length = j + 1 - entity.Offset
|
2022-03-12 01:27:15 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if dirty {
|
|
|
|
alignedEntities[i] = entity
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return alignedEntities
|
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion {
|
|
|
|
return []*insertion{
|
|
|
|
&insertion{
|
2020-01-09 21:16:40 +00:00
|
|
|
Offset: entity.Offset,
|
|
|
|
Runes: lbrace,
|
2023-11-16 00:38:45 +00:00
|
|
|
Type: insertionOpening,
|
|
|
|
},
|
|
|
|
&insertion{
|
2020-01-09 21:16:40 +00:00
|
|
|
Offset: entity.Offset + entity.Length,
|
|
|
|
Runes: rbrace,
|
2023-11-16 00:38:45 +00:00
|
|
|
Type: insertionClosing,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func quotePrependNewlines(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
|
|
|
|
if len(doubledRunes) == 0 {
|
|
|
|
return []*insertion{}
|
|
|
|
}
|
|
|
|
|
|
|
|
startRunes := []rune("\n> ")
|
|
|
|
if entity.Offset == 0 || doubledRunes[entity.Offset-1] == newlineCode {
|
|
|
|
startRunes = quoteRunes
|
|
|
|
}
|
|
|
|
insertions := []*insertion{
|
|
|
|
&insertion{
|
|
|
|
Offset: entity.Offset,
|
|
|
|
Runes: startRunes,
|
|
|
|
Type: insertionUnpaired,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
entityEnd := entity.Offset + entity.Length
|
|
|
|
entityEndInt := int(entityEnd)
|
|
|
|
|
|
|
|
var wasNewline bool
|
|
|
|
// last newline is omitted, there's no need to put quote mark after the quote
|
|
|
|
for i := entity.Offset; i < entityEnd-1; i++ {
|
|
|
|
isNewline := doubledRunes[i] == newlineCode
|
|
|
|
if (isNewline && markupMode == MarkupModeXEP0393) || (wasNewline && isNewline && markupMode == MarkupModeMarkdown) {
|
|
|
|
insertions = append(insertions, &insertion{
|
|
|
|
Offset: i+1,
|
|
|
|
Runes: quoteRunes,
|
|
|
|
Type: insertionUnpaired,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if isNewline {
|
|
|
|
wasNewline = true
|
|
|
|
} else {
|
|
|
|
wasNewline = false
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
2023-11-16 00:38:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var rbrace []rune
|
|
|
|
if len(doubledRunes) > entityEndInt {
|
|
|
|
if doubledRunes[entityEnd] == newlineCode {
|
|
|
|
if markupMode == MarkupModeMarkdown && len(doubledRunes) > entityEndInt+1 && doubledRunes[entityEndInt+1] != newlineCode {
|
|
|
|
rbrace = newlineRunes
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if markupMode == MarkupModeMarkdown {
|
|
|
|
rbrace = doubleNewlineRunes
|
|
|
|
} else {
|
|
|
|
rbrace = newlineRunes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
insertions = append(insertions, &insertion{
|
|
|
|
Offset: entityEnd,
|
|
|
|
Runes: rbrace,
|
|
|
|
Type: insertionClosing,
|
|
|
|
})
|
|
|
|
|
|
|
|
return insertions
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
// entityToMarkdown generates the wrapping Markdown tags
|
|
|
|
func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
|
|
|
|
if entity == nil || entity.Type == nil {
|
|
|
|
return []*insertion{}
|
|
|
|
}
|
|
|
|
|
2020-01-09 21:16:40 +00:00
|
|
|
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)
|
2022-03-11 17:54:03 +00:00
|
|
|
case client.TypeTextEntityTypeStrikethrough:
|
|
|
|
return markupBraces(entity, strikeRunesMarkdown, strikeRunesMarkdown)
|
2022-03-11 17:01:38 +00:00
|
|
|
case client.TypeTextEntityTypeCode:
|
2020-01-09 21:16:40 +00:00
|
|
|
return markupBraces(entity, codeRunes, codeRunes)
|
2022-03-11 17:01:38 +00:00
|
|
|
case client.TypeTextEntityTypePre:
|
|
|
|
return markupBraces(entity, preRuneStart, preRuneEnd)
|
2020-01-09 21:16:40 +00:00
|
|
|
case client.TypeTextEntityTypePreCode:
|
|
|
|
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
|
|
|
|
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
|
2023-11-16 00:38:45 +00:00
|
|
|
case client.TypeTextEntityTypeBlockQuote:
|
|
|
|
return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown)
|
2020-01-09 21:16:40 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
return []*insertion{}
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
// entityToXEP0393 generates the wrapping XEP-0393 tags
|
|
|
|
func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
|
2022-02-18 23:41:08 +00:00
|
|
|
if entity == nil || entity.Type == nil {
|
2023-11-16 00:38:45 +00:00
|
|
|
return []*insertion{}
|
2022-02-18 23:41:08 +00:00
|
|
|
}
|
|
|
|
|
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)
|
2022-03-11 17:54:03 +00:00
|
|
|
case client.TypeTextEntityTypeStrikethrough:
|
|
|
|
return markupBraces(entity, strikeRunesXEP0393, strikeRunesXEP0393)
|
2022-03-11 17:01:38 +00:00
|
|
|
case client.TypeTextEntityTypeCode:
|
2021-12-18 16:04:24 +00:00
|
|
|
return markupBraces(entity, codeRunes, codeRunes)
|
2022-03-11 17:01:38 +00:00
|
|
|
case client.TypeTextEntityTypePre:
|
|
|
|
return markupBraces(entity, preRuneStart, preRuneEnd)
|
2021-12-18 16:04:24 +00:00
|
|
|
case client.TypeTextEntityTypePreCode:
|
|
|
|
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
|
|
|
|
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
|
2023-11-16 00:38:45 +00:00
|
|
|
case client.TypeTextEntityTypeBlockQuote:
|
|
|
|
return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393)
|
2021-12-18 16:04:24 +00:00
|
|
|
case client.TypeTextEntityTypeTextUrl:
|
|
|
|
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
|
|
|
|
// non-standard, Pidgin-specific
|
|
|
|
return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">"))
|
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
return []*insertion{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// transform the source text into a form with uniform runes and code points,
|
|
|
|
// by duplicating anything beyond the Basic Multilingual Plane
|
|
|
|
func textToDoubledRunes(text string) []rune {
|
|
|
|
doubledRunes := make([]rune, 0, len(text)*2)
|
|
|
|
for _, cp := range text {
|
|
|
|
if cp > bmpCeil {
|
|
|
|
doubledRunes = append(doubledRunes, cp, cp)
|
|
|
|
} else {
|
|
|
|
doubledRunes = append(doubledRunes, cp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return doubledRunes
|
2021-12-18 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
2023-11-16 00:38:45 +00:00
|
|
|
markupMode MarkupModeType,
|
2020-01-09 21:16:40 +00:00
|
|
|
) string {
|
|
|
|
if len(entities) == 0 {
|
|
|
|
return sourceText
|
|
|
|
}
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
var entityToMarkup func(*client.TextEntity, []rune, MarkupModeType) []*insertion
|
|
|
|
if markupMode == MarkupModeXEP0393 {
|
|
|
|
entityToMarkup = entityToXEP0393
|
|
|
|
} else {
|
|
|
|
entityToMarkup = entityToMarkdown
|
|
|
|
}
|
2022-03-11 16:12:36 +00:00
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
doubledRunes := textToDoubledRunes(sourceText)
|
|
|
|
|
|
|
|
mergedEntities := SortEntities(ClaspDirectives(doubledRunes, MergeAdjacentEntities(SortEntities(entities))))
|
|
|
|
|
|
|
|
startStack := make(insertionStack, 0, len(sourceText))
|
|
|
|
endStack := make(insertionStack, 0, len(sourceText))
|
2020-01-09 21:16:40 +00:00
|
|
|
|
|
|
|
// convert entities to a stack of brackets
|
|
|
|
var maxEndOffset int32
|
2022-03-11 16:12:36 +00:00
|
|
|
for _, entity := range mergedEntities {
|
2020-01-09 21:16:40 +00:00
|
|
|
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)
|
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
insertions := entityToMarkup(entity, doubledRunes, markupMode)
|
|
|
|
if len(insertions) > 1 {
|
|
|
|
startStack = append(startStack, insertions[0:len(insertions)-1]...)
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
2023-11-16 00:38:45 +00:00
|
|
|
if len(insertions) > 0 {
|
|
|
|
endStack = append(endStack, insertions[len(insertions)-1])
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// flush the closing brackets that still remain in endStack
|
|
|
|
startStack, endStack = startStack.rebalance(endStack, maxEndOffset)
|
2023-11-16 00:38:45 +00:00
|
|
|
// sort unpaired insertions
|
|
|
|
sort.SliceStable(startStack, func(i int, j int) bool {
|
|
|
|
ins1 := startStack[i]
|
|
|
|
ins2 := startStack[j]
|
|
|
|
if ins1.Type == insertionUnpaired && ins2.Type == insertionUnpaired {
|
|
|
|
return ins1.Offset < ins2.Offset
|
|
|
|
}
|
|
|
|
if ins1.Type == insertionUnpaired {
|
|
|
|
if ins1.Offset == ins2.Offset {
|
|
|
|
if ins2.Type == insertionOpening { // > **
|
|
|
|
return true
|
|
|
|
} else if ins2.Type == insertionClosing { // **>
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ins1.Offset < ins2.Offset
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ins2.Type == insertionUnpaired {
|
|
|
|
if ins1.Offset == ins2.Offset {
|
|
|
|
if ins1.Type == insertionOpening { // > **
|
|
|
|
return false
|
|
|
|
} else if ins1.Type == insertionClosing { // **>
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ins1.Offset < ins2.Offset
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
2020-01-09 21:16:40 +00:00
|
|
|
|
|
|
|
// merge brackets into text
|
|
|
|
markupRunes := make([]rune, 0, len(sourceText))
|
|
|
|
|
|
|
|
nextInsertion := startStack.NewIterator()
|
|
|
|
insertion := nextInsertion()
|
2023-11-16 00:38:45 +00:00
|
|
|
var skipNext bool
|
2020-01-09 21:16:40 +00:00
|
|
|
|
2023-11-16 00:38:45 +00:00
|
|
|
for i, cp := range doubledRunes {
|
|
|
|
if skipNext {
|
|
|
|
skipNext = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for insertion != nil && int(insertion.Offset) <= i {
|
2020-01-09 21:16:40 +00:00
|
|
|
markupRunes = append(markupRunes, insertion.Runes...)
|
|
|
|
insertion = nextInsertion()
|
|
|
|
}
|
|
|
|
|
|
|
|
markupRunes = append(markupRunes, cp)
|
|
|
|
// skip two UTF-16 code units (not points actually!) if needed
|
2023-11-16 00:38:45 +00:00
|
|
|
if cp > bmpCeil {
|
|
|
|
skipNext = true
|
2020-01-09 21:16:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for insertion != nil {
|
|
|
|
markupRunes = append(markupRunes, insertion.Runes...)
|
|
|
|
insertion = nextInsertion()
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(markupRunes)
|
|
|
|
}
|