mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-11-28 07:21:21 +08:00
pkg/ui: Implement ParseSGREscapedText.
Part of a reimplementation of #1038.
This commit is contained in:
parent
9169f803ea
commit
5e648ceac7
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}),
|
||||
|
|
Loading…
Reference in New Issue
Block a user