pkg/ui: Implement ParseSGREscapedText.

Part of a reimplementation of #1038.
This commit is contained in:
Qi Xiao 2020-08-25 22:47:22 +01:00
parent 9169f803ea
commit 5e648ceac7
2 changed files with 108 additions and 4 deletions

View File

@ -3,8 +3,88 @@ package ui
import (
"strconv"
"strings"
"unicode"
)
type sgrTokenizer struct {
text string
styling Styling
content string
}
const sgrPrefix = "\033["
func (st *sgrTokenizer) Next() bool {
for strings.HasPrefix(st.text, sgrPrefix) {
trimmed := strings.TrimPrefix(st.text, sgrPrefix)
// Find the terminator of this sequence.
termIndex := strings.IndexFunc(trimmed, func(r rune) bool {
return r != ';' && (r < '0' || r > '9')
})
if termIndex == -1 {
// The string ends with an unterminated escape sequence; ignore
// it.
st.text = ""
return false
}
term := trimmed[termIndex]
sgr := trimmed[:termIndex]
st.text = trimmed[termIndex+1:]
if term == 'm' {
st.styling = StylingFromSGR(sgr)
st.content = ""
return true
}
// If the terminator is not 'm'; we have seen a non-SGR escape sequence;
// ignore it and continue.
}
if st.text == "" {
return false
}
// Parse a content segment until the next SGR prefix.
content := ""
nextSGR := strings.Index(st.text, sgrPrefix)
if nextSGR == -1 {
content = st.text
} else {
content = st.text[:nextSGR]
}
st.text = st.text[len(content):]
st.styling = nil
var sb strings.Builder
for _, r := range content {
if unicode.IsGraphic(r) {
sb.WriteRune(r)
}
}
st.content = sb.String()
return true
}
func (st *sgrTokenizer) Token() (Styling, string) {
return st.styling, st.content
}
// ParseSGREscapedText parses SGR-escaped text into a Text. It also removes
// other unprintable sequences in the text.
func ParseSGREscapedText(s string) Text {
var text Text
var style Style
tokenizer := sgrTokenizer{text: s}
for tokenizer.Next() {
styling, content := tokenizer.Token()
if styling != nil {
styling.transform(&style)
}
if content != "" {
text = append(text, &Segment{style, content})
}
}
return text
}
var sgrStyling = map[int]Styling{
0: Reset,
1: Bold,
@ -25,6 +105,9 @@ func StyleFromSGR(s string) Style {
func StylingFromSGR(s string) Styling {
styling := jointStyling{}
codes := getSGRCodes(s)
if len(codes) == 0 {
return Reset
}
for len(codes) > 0 {
code := codes[0]
consume := 1
@ -69,11 +152,14 @@ func StylingFromSGR(s string) Styling {
func getSGRCodes(s string) []int {
var codes []int
for _, part := range strings.Split(s, ";") {
code, err := strconv.Atoi(part)
if err != nil {
continue
if part == "" {
codes = append(codes, 0)
} else {
code, err := strconv.Atoi(part)
if err == nil {
codes = append(codes, code)
}
}
codes = append(codes, code)
}
return codes
}

View File

@ -6,6 +6,24 @@ import (
"github.com/elves/elvish/pkg/tt"
)
func TestParseSGREscapedText(t *testing.T) {
tt.Test(t, tt.Fn("ParseSGREscapedText", ParseSGREscapedText), tt.Table{
tt.Args("").Rets(Text(nil)),
tt.Args("text").Rets(T("text")),
tt.Args("\033[1mbold").Rets(T("bold", Bold)),
tt.Args("\033[1mbold\033[31mbold red").Rets(
Concat(T("bold", Bold), T("bold red", Bold, FgRed))),
tt.Args("\033[1mbold\033[;31mred").Rets(
Concat(T("bold", Bold), T("red", FgRed))),
// Other escape sequences are ignored.
tt.Args("\033[Atext").Rets(T("text")),
// Non-graphic runes are removed.
tt.Args("t\x01ext").Rets(T("text")),
// Lone escape runes are removed.
tt.Args("t\033ext").Rets(T("text")),
})
}
func TestStyleFromSGR(t *testing.T) {
tt.Test(t, tt.Fn("StyleFromSGR", StyleFromSGR), tt.Table{
tt.Args("1").Rets(Style{Bold: true}),