Added EscapeCodeRenderer

- Added `EscapeCode`-type
 - Implemented EscapeCode.String()
 - Implemented EscapeCode.Raw()
 - Implemented EscapeCode.MakeSafe()
 - Implemented EscapeCode.IsValid()

 - Added `EscapeCodeRenderer`
 - Implemented EscapeCodeRenderer.RenderSequence()
 - Implemented EscapeCodeRenderer.Render()
 - Implemented `EscapeCodeRenderer.NormalizedText`.

 - Added EscapeCodeRendererFactory
 - Implemented EscapeCodeRendererFactory.TextRenderer()
 - Added escape code examples to examples/coloredList.go
This commit is contained in:
Matteo Kloiber
2015-04-10 23:12:28 +02:00
parent ac747cb49f
commit a3f1384a3b
3 changed files with 431 additions and 9 deletions

View File

@@ -1,7 +1,9 @@
package termui
import (
"fmt"
"regexp"
"strconv"
"strings"
)
@@ -243,3 +245,210 @@ type PlainRendererFactory struct{}
func (f PlainRendererFactory) TextRenderer(text string) TextRenderer {
return PlainRenderer{text}
}
// We can't use a raw string here because \033 must not be escaped.
// I'd like to append (?<=m; i.e. lookbehind), but unfortunately,
// it is not supported. So we will need to do that manually.
var escapeRegex = "\033\\[(([0-9]{1,2}[;m])+)"
var colorEscapeCodeRegex = regexp.MustCompile(escapeRegex)
var colorEscapeCodeRegexMatchAll = regexp.MustCompile("^" + escapeRegex + "$")
// An EscapeCode is a unix ASCII Escape code.
type EscapeCode string
func (e EscapeCode) escapeNumberToColor(colorID int) (Attribute, error) {
var color Attribute
switch colorID {
case 0:
color = ColorDefault
case 1:
color = AttrBold
case 4:
color = AttrUnderline
case 30:
color = ColorBlack
case 31:
color = ColorRed
case 32:
color = ColorGreen
case 33:
color = ColorYellow
case 34:
color = ColorBlue
case 35:
color = ColorMagenta
case 36:
color = ColorCyan
case 37:
color = ColorWhite
default:
safeCode := e.MakeSafe()
return 0, fmt.Errorf("Unkown/unsupported escape code: '%v'", safeCode)
}
return color, nil
}
// Color converts the escape code to an `Attribute` (color).
// The EscapeCode must be formatted like this:
// - ASCII-Escape chacter (\033) + [ + Number + (;Number...) + m
// The second number is optimal. The semicolon (;) is used
// to seperate the colors.
// For example: `\033[1;31m` means: the following text is red and bold.
func (e EscapeCode) Color() (Attribute, error) {
escapeCode := string(e)
matches := colorEscapeCodeRegexMatchAll.FindStringSubmatch(escapeCode)
invalidEscapeCode := func() error {
safeCode := e.MakeSafe()
return fmt.Errorf("%v is not a valid ASCII escape code", safeCode)
}
if matches == nil || escapeCode[len(escapeCode)-1] != 'm' {
return 0, invalidEscapeCode()
}
color := Attribute(0)
for _, id := range strings.Split(matches[1][:len(matches[1])-1], ";") {
colorID, err := strconv.Atoi(id)
if err != nil {
return 0, invalidEscapeCode()
}
newColor, err := e.escapeNumberToColor(colorID)
if err != nil {
return 0, err
}
color |= newColor
}
return color, nil
}
// MakeSafe replace the invisible escape code chacacter (\0333)
// with \\0333 so that it will not mess up the terminal when an error
// is shown.
func (e EscapeCode) MakeSafe() string {
return strings.Replace(string(e), "\033", "\\033", -1)
}
// Alias to `EscapeCode.MakeSafe()`
func (e EscapeCode) String() string {
return e.MakeSafe()
}
// Raw returns the raw value of the escape code.
// Alias to string(EscapeCode)
func (e EscapeCode) Raw() string {
return string(e)
}
// IsValid returns whether or not the syntax of the escape code is
// valid and the code is supported.
func (e EscapeCode) IsValid() bool {
_, err := e.Color()
return err == nil
}
// A EscapeCodeRenderer does not render the text at all.
type EscapeCodeRenderer struct {
Text string
}
// NormalizedText strips all escape code outs (even the unkown/unsupported)
// ones.
func (r EscapeCodeRenderer) NormalizedText() string {
matches := colorEscapeCodeRegex.FindAllStringIndex(r.Text, -1)
text := []byte(r.Text)
// Iterate through matches in reverse order
for i := len(matches) - 1; i >= 0; i-- {
start, end := matches[i][0], matches[i][1]
if EscapeCode(text[start:end]).IsValid() {
text = append(text[:start], text[end:]...)
}
}
return string(text)
}
// RenderSequence renders the text just like Render but the start and end may
// be set. If end is -1, the end of the string will be used.
func (r EscapeCodeRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
normalizedRunes := []rune(r.NormalizedText())
if end < 0 {
end = len(normalizedRunes)
}
text := []byte(r.Text)
matches := colorEscapeCodeRegex.FindAllSubmatchIndex(text, -1)
removed := 0
var sequences []ColorSubsequence
runeLength := func(length int) int {
return len([]rune(string(text[:length])))
}
runes := []rune(r.Text)
for _, theMatch := range matches {
// Escapde code start, escape code end
eStart := runeLength(theMatch[0]) - removed
eEnd := runeLength(theMatch[1]) - removed
escapeCode := EscapeCode(runes[eStart:eEnd])
// If an error occurs (e.g. unkown escape code), we will just ignore it :)
color, err := escapeCode.Color()
if err != nil {
continue
}
// Patch old color sequence
if len(sequences) > 0 {
last := &sequences[len(sequences)-1]
last.End = eStart - start
}
// eEnd < 0 means the the sequence is withing the range.
if eEnd-start >= 0 {
// The sequence starts when the escape code ends and ends when the text
// end. If there is another escape code, this will be patched in the
// previous line.
colorSeq := ColorSubsequence{color, eStart - start, end - start}
if colorSeq.Start < 0 {
colorSeq.Start = 0
}
sequences = append(sequences, colorSeq)
}
runes = append(runes[:eStart], runes[eEnd:]...)
removed += eEnd - eStart
}
runes = runes[start:end]
return RenderedSequence{string(runes), lastColor, background, sequences, nil}
}
// Render just like RenderSequence
func (r EscapeCodeRenderer) Render(lastColor, background Attribute) RenderedSequence {
return r.RenderSequence(0, -1, lastColor, background)
}
// EscapeCodeRendererFactory is a TextRendererFactory for
// the EscapeCodeRenderer.
type EscapeCodeRendererFactory struct{}
// TextRenderer returns a EscapeCodeRenderer instance.
func (f EscapeCodeRendererFactory) TextRenderer(text string) TextRenderer {
return EscapeCodeRenderer{text}
}