Switched string iteration to using the github.com/rivo/uniseg package.

This commit is contained in:
Oliver 2019-03-14 12:47:53 +01:00
parent 3938f60085
commit 2cc825800b
1 changed files with 41 additions and 81 deletions

122
util.go
View File

@ -5,10 +5,10 @@ import (
"regexp"
"sort"
"strconv"
"unicode"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// Text alignment within a box.
@ -528,51 +528,24 @@ func Escape(text string) string {
// returns true. This function returns true if the iteration was stopped before
// the last character.
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
var (
runes []rune
lastZeroWidthJoiner bool
startIndex int
startPos int
pos int
)
var screenPos int
// Helper function which invokes the callback.
flush := func(index int) bool {
gr := uniseg.NewGraphemes(text)
for gr.Next() {
r := gr.Runes()
from, to := gr.Positions()
width := runewidth.StringWidth(gr.Str())
var comb []rune
if len(runes) > 1 {
comb = runes[1:]
if len(r) > 1 {
comb = r[1:]
}
return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos)
}
for index, r := range text {
if unicode.In(r, unicode.M) || r == '\u200d' {
lastZeroWidthJoiner = r == '\u200d'
} else {
// We have a rune that's not a modifier. It could be the beginning of a
// new character.
if !lastZeroWidthJoiner {
if len(runes) > 0 {
// It is. Invoke callback.
if flush(index) {
return true // We're done.
}
// Reset rune store.
runes = runes[:0]
startIndex = index
startPos = pos
}
pos += runewidth.RuneWidth(r)
} else {
lastZeroWidthJoiner = false
}
// panic(fmt.Sprintf(`from=%d to=%d screenPos=%d width=%d`, from, to, screenPos, width))
if callback(r[0], comb, from, to-from, screenPos, width) {
return true
}
runes = append(runes, r)
}
// Flush any remaining runes.
if len(runes) > 0 {
flush(len(text))
screenPos += width
}
return false
@ -587,50 +560,37 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t
// screen width of it. The iteration stops if the callback returns true. This
// function returns true if the iteration was stopped before the last character.
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
type runePos struct {
r rune
pos int // The byte position of the rune in the original string.
width int // The screen width of the rune.
mod bool // Modifier or zero-width-joiner.
type cluster struct {
main rune
comb []rune
textPos, textWidth, screenPos, screenWidth int
}
// We use the following:
// len(text) >= number of runes in text.
// Create the grapheme clusters.
var clusters []cluster
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
clusters = append(clusters, cluster{
main: main,
comb: comb,
textPos: textPos,
textWidth: textWidth,
screenPos: screenPos,
screenWidth: screenWidth,
})
return false
})
// Put all runes into a runePos slice in reverse.
runesReverse := make([]runePos, len(text))
index := len(text) - 1
for pos, ch := range text {
runesReverse[index].r = ch
runesReverse[index].pos = pos
runesReverse[index].width = runewidth.RuneWidth(ch)
runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d'
index--
}
runesReverse = runesReverse[index+1:]
// Parse reverse runes.
var screenWidth int
buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again.
bufferPos := len(text)
stringWidth := runewidth.StringWidth(text)
for index, r := range runesReverse {
// Put this rune into the buffer.
bufferPos--
buffer[bufferPos] = r.r
// Do we need to flush the buffer?
if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' {
// Yes, invoke callback.
var comb []rune
if len(text)-bufferPos > 1 {
comb = buffer[bufferPos+1:]
}
if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) {
return true
}
screenWidth += r.width
bufferPos = len(text)
// Iterate in reverse.
for index := len(clusters) - 1; index >= 0; index-- {
if callback(
clusters[index].main,
clusters[index].comb,
clusters[index].textPos,
clusters[index].textWidth,
clusters[index].screenPos,
clusters[index].screenWidth,
) {
return true
}
}