Finish TextBuilder
This commit is contained in:
parent
62105f6883
commit
7f94c273e5
533
textRender.go
533
textRender.go
@ -1,533 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Minial interface
|
|
||||||
type TextBuilder interface {
|
|
||||||
Build(s string, fg, bg Attribute) []Cells
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkdownTxBuilder struct {
|
|
||||||
regex string
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
baseFg Attribute
|
|
||||||
baseBg Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
var colorMap = map[string]Attribute{
|
|
||||||
"red": ColorRed,
|
|
||||||
"blue": ColorBlue,
|
|
||||||
"black": ColorBlack,
|
|
||||||
"cyan": ColorCyan,
|
|
||||||
"white": ColorWhite,
|
|
||||||
"default": ColorDefault,
|
|
||||||
"green": ColorGreen,
|
|
||||||
"magenta": ColorMagenta,
|
|
||||||
}
|
|
||||||
|
|
||||||
var attrMap = map[string]Attribute{
|
|
||||||
"bold": AttrBold,
|
|
||||||
"underline": AttrUnderline,
|
|
||||||
"reverse": AttrReverse,
|
|
||||||
}
|
|
||||||
|
|
||||||
func rmSpc(s string) string {
|
|
||||||
reg := regexp.MustCompile(`\s+`)
|
|
||||||
return reg.ReplaceAllString(s, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
|
|
||||||
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
|
|
||||||
fg := mtb.baseFg
|
|
||||||
bg := mtb.baseBg
|
|
||||||
|
|
||||||
updateAttr := func(a Attribute, attrs []string) Attribute {
|
|
||||||
for _, s := range attrs {
|
|
||||||
if c, ok := colorMap[s]; ok {
|
|
||||||
a &= ^(1<<9 - 1) //erase clr 0 ~ 1<<9-1
|
|
||||||
a |= c // set clr
|
|
||||||
}
|
|
||||||
if c, ok := attrMap[s]; ok {
|
|
||||||
a |= c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
ss := strings.Split(s, ",")
|
|
||||||
fgs := []string{}
|
|
||||||
bgs := []string{}
|
|
||||||
for _, v := range ss {
|
|
||||||
subs := strings.Split(ss, "-")
|
|
||||||
if len(subs) > 1 {
|
|
||||||
if subs[0] == "fg" {
|
|
||||||
fgs := append(fgs, subs[1])
|
|
||||||
}
|
|
||||||
if subs[0] == "bg" {
|
|
||||||
bgs := append(bgs, subs[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fg = updateAttr(fg)
|
|
||||||
bg = updateAttr(bg)
|
|
||||||
return fg, bg
|
|
||||||
}
|
|
||||||
|
|
||||||
type EscCodeTxBuilder struct {
|
|
||||||
regex string
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextRenderer adds common methods for rendering a text on screeen.
|
|
||||||
type TextRenderer interface {
|
|
||||||
NormalizedText() string
|
|
||||||
Render(lastColor, background Attribute) RenderedSequence
|
|
||||||
RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextRendererFactory is factory for creating text renderers.
|
|
||||||
type TextRendererFactory interface {
|
|
||||||
TextRenderer(text string) TextRenderer
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownRegex is used by MarkdownTextRenderer to determine how to format the
|
|
||||||
// text.
|
|
||||||
const MarkdownRegex = `(?:\[([^]]+)\])\(([a-z\s,]+)\)`
|
|
||||||
|
|
||||||
// unexported because a pattern can't be a constant and we don't want anyone
|
|
||||||
// messing with the regex.
|
|
||||||
var markdownPattern = regexp.MustCompile(MarkdownRegex)
|
|
||||||
|
|
||||||
// MarkdownTextRenderer is used for rendering the text with colors using
|
|
||||||
// markdown-like syntax.
|
|
||||||
// See: https://github.com/gizak/termui/issues/4#issuecomment-87270635
|
|
||||||
type MarkdownTextRenderer struct {
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NormalizedText returns the text the user will see (without colors).
|
|
||||||
// It strips out all formatting option and only preserves plain text.
|
|
||||||
func (r MarkdownTextRenderer) NormalizedText() string {
|
|
||||||
return r.normalizeText(r.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MarkdownTextRenderer) normalizeText(text string) string {
|
|
||||||
lText := strings.ToLower(text)
|
|
||||||
indexes := markdownPattern.FindAllStringSubmatchIndex(lText, -1)
|
|
||||||
|
|
||||||
// Interate through indexes in reverse order.
|
|
||||||
for i := len(indexes) - 1; i >= 0; i-- {
|
|
||||||
theIndex := indexes[i]
|
|
||||||
start, end := theIndex[0], theIndex[1]
|
|
||||||
contentStart, contentEnd := theIndex[2], theIndex[3]
|
|
||||||
|
|
||||||
text = text[:start] + text[contentStart:contentEnd] + text[end:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the position considering unicode characters.
|
|
||||||
func posUnicode(text string, pos int) int {
|
|
||||||
return len([]rune(text[:pos]))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Render renders the sequence `text` using a markdown-like syntax:
|
|
||||||
`[hello](red) world` will become: `hello world` where hello is red.
|
|
||||||
|
|
||||||
You may also specify other attributes such as bold text:
|
|
||||||
`[foo](YELLOW, BOLD)` will become `foo` in yellow, bold text.
|
|
||||||
|
|
||||||
|
|
||||||
For all available combinations, colors, and attribute, see: `StringToAttribute`.
|
|
||||||
|
|
||||||
This method returns a RenderedSequence
|
|
||||||
*/
|
|
||||||
func (r MarkdownTextRenderer) Render(lastColor, background Attribute) RenderedSequence {
|
|
||||||
return r.RenderSequence(0, -1, lastColor, background)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderSequence renders the text just like Render but the start and end can
|
|
||||||
// be specified.
|
|
||||||
func (r MarkdownTextRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
|
|
||||||
text := r.Text
|
|
||||||
if end == -1 {
|
|
||||||
end = len([]rune(r.NormalizedText()))
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatch := func(s string) []int {
|
|
||||||
return markdownPattern.FindStringSubmatchIndex(strings.ToLower(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
var sequences []ColorSubsequence
|
|
||||||
for match := getMatch(text); match != nil; match = getMatch(text) {
|
|
||||||
// Check if match is in the start/end range.
|
|
||||||
|
|
||||||
matchStart, matchEnd := match[0], match[1]
|
|
||||||
colorStart, colorEnd := match[4], match[5]
|
|
||||||
contentStart, contentEnd := match[2], match[3]
|
|
||||||
|
|
||||||
color := StringToAttribute(text[colorStart:colorEnd])
|
|
||||||
content := text[contentStart:contentEnd]
|
|
||||||
theSequence := ColorSubsequence{color, contentStart - 1, contentEnd - 1}
|
|
||||||
theSequence.Start = posUnicode(text, contentStart) - 1
|
|
||||||
theSequence.End = posUnicode(text, contentEnd) - 1
|
|
||||||
|
|
||||||
if start < theSequence.End && end > theSequence.Start {
|
|
||||||
// Make the sequence relative and append.
|
|
||||||
theSequence.Start -= start
|
|
||||||
if theSequence.Start < 0 {
|
|
||||||
theSequence.Start = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
theSequence.End -= start
|
|
||||||
if theSequence.End < 0 {
|
|
||||||
theSequence.End = 0
|
|
||||||
} else if theSequence.End > end-start {
|
|
||||||
theSequence.End = end - start
|
|
||||||
}
|
|
||||||
|
|
||||||
sequences = append(sequences, theSequence)
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text[:matchStart] + content + text[matchEnd:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if end == -1 {
|
|
||||||
end = len(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
runes := []rune(text)[start:end]
|
|
||||||
return RenderedSequence{string(runes), lastColor, background, sequences, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownTextRendererFactory is a TextRendererFactory for
|
|
||||||
// the MarkdownTextRenderer.
|
|
||||||
type MarkdownTextRendererFactory struct{}
|
|
||||||
|
|
||||||
// TextRenderer returns a MarkdownTextRenderer instance.
|
|
||||||
func (f MarkdownTextRendererFactory) TextRenderer(text string) TextRenderer {
|
|
||||||
return MarkdownTextRenderer{text}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderedSequence is a string sequence that is capable of returning the
|
|
||||||
// Buffer used by termui for displaying the colorful string.
|
|
||||||
type RenderedSequence struct {
|
|
||||||
NormalizedText string
|
|
||||||
LastColor Attribute
|
|
||||||
BackgroundColor Attribute
|
|
||||||
Sequences []ColorSubsequence
|
|
||||||
|
|
||||||
// Use the color() method for getting the correct value.
|
|
||||||
mapSequences map[int]Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// A ColorSubsequence represents a color for the given text span.
|
|
||||||
type ColorSubsequence struct {
|
|
||||||
Color Attribute
|
|
||||||
Start int
|
|
||||||
End int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColorSubsequencesToMap creates a map with all colors that from the
|
|
||||||
// subsequences.
|
|
||||||
func ColorSubsequencesToMap(sequences []ColorSubsequence) map[int]Attribute {
|
|
||||||
result := make(map[int]Attribute)
|
|
||||||
for _, theSequence := range sequences {
|
|
||||||
for i := theSequence.Start; i < theSequence.End; i++ {
|
|
||||||
result[i] = theSequence.Color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RenderedSequence) colors() map[int]Attribute {
|
|
||||||
if s.mapSequences == nil {
|
|
||||||
s.mapSequences = ColorSubsequencesToMap(s.Sequences)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.mapSequences
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer returns the colorful formatted buffer and the last color that was
|
|
||||||
// used.
|
|
||||||
func (s *RenderedSequence) Buffer(x, y int) ([]Point, Attribute) {
|
|
||||||
buffer := make([]Point, 0, len(s.NormalizedText)) // This is just an assumtion
|
|
||||||
|
|
||||||
for i := range []rune(s.NormalizedText) {
|
|
||||||
p, width := s.PointAt(i, x, y)
|
|
||||||
buffer = append(buffer, p)
|
|
||||||
x += width
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer, s.LastColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// PointAt returns the point at the position of n. The x, and y coordinates
|
|
||||||
// are used for placing the point at the right position.
|
|
||||||
// Since some charaters are wider (like `一`), this method also returns the
|
|
||||||
// width the point actually took.
|
|
||||||
// This is important for increasing the x property properly.
|
|
||||||
func (s *RenderedSequence) PointAt(n, x, y int) (Point, int) {
|
|
||||||
color, ok := s.colors()[n]
|
|
||||||
if !ok {
|
|
||||||
color = s.LastColor
|
|
||||||
}
|
|
||||||
|
|
||||||
char := []rune(s.NormalizedText)[n]
|
|
||||||
return Point{char, s.BackgroundColor, color, x, y}, charWidth(char)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A PlainRenderer does not render the text at all.
|
|
||||||
type PlainRenderer struct {
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NormalizedText returns the text given in
|
|
||||||
func (r PlainRenderer) NormalizedText() string {
|
|
||||||
return r.Text
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderSequence returns a RenderedSequence that does not have any color
|
|
||||||
// sequences.
|
|
||||||
func (r PlainRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
|
|
||||||
runes := []rune(r.Text)
|
|
||||||
if end < 0 {
|
|
||||||
end = len(runes)
|
|
||||||
}
|
|
||||||
|
|
||||||
runes = runes[start:end]
|
|
||||||
var s []ColorSubsequence
|
|
||||||
return RenderedSequence{string(runes), lastColor, background, s, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render just like RenderSequence
|
|
||||||
func (r PlainRenderer) Render(lastColor, background Attribute) RenderedSequence {
|
|
||||||
return r.RenderSequence(0, -1, lastColor, background)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlainRendererFactory is a TextRendererFactory for
|
|
||||||
// the PlainRenderer.
|
|
||||||
type PlainRendererFactory struct{}
|
|
||||||
|
|
||||||
// TextRenderer returns a PlainRenderer instance.
|
|
||||||
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}
|
|
||||||
}
|
|
@ -1,454 +0,0 @@
|
|||||||
package termui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTextRender_TestInterface(t *testing.T) {
|
|
||||||
var inter *TextRenderer
|
|
||||||
|
|
||||||
assert.Implements(t, inter, new(MarkdownTextRenderer))
|
|
||||||
assert.Implements(t, inter, new(EscapeCodeRenderer))
|
|
||||||
assert.Implements(t, inter, new(PlainRenderer))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextRendererFactory_TestInterface(t *testing.T) {
|
|
||||||
var inter *TextRendererFactory
|
|
||||||
|
|
||||||
assert.Implements(t, inter, new(MarkdownTextRendererFactory))
|
|
||||||
assert.Implements(t, inter, new(EscapeCodeRendererFactory))
|
|
||||||
assert.Implements(t, inter, new(PlainRendererFactory))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdownTextRenderer_normalizeText(t *testing.T) {
|
|
||||||
renderer := MarkdownTextRenderer{}
|
|
||||||
|
|
||||||
got := renderer.normalizeText("[ERROR](red,bold) Something went wrong")
|
|
||||||
assert.Equal(t, got, "ERROR Something went wrong")
|
|
||||||
|
|
||||||
got = renderer.normalizeText("[foo](red) hello [bar](green) world")
|
|
||||||
assert.Equal(t, got, "foo hello bar world")
|
|
||||||
|
|
||||||
got = renderer.normalizeText("[foo](g) hello [bar]green (world)")
|
|
||||||
assert.Equal(t, got, "foo hello [bar]green (world)")
|
|
||||||
|
|
||||||
got = "笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺"
|
|
||||||
expected := "笀耔 澉 灊灅甗 郔镺 笀耔 澉 灊灅甗 郔镺"
|
|
||||||
assert.Equal(t, renderer.normalizeText(got), expected)
|
|
||||||
|
|
||||||
got = renderer.normalizeText("[(foo)](red,white) bar")
|
|
||||||
assert.Equal(t, renderer.normalizeText(got), "(foo) bar")
|
|
||||||
|
|
||||||
// TODO: make this regex work correctly:
|
|
||||||
// got = renderer.normalizeText("[[foo]](red,white) bar")
|
|
||||||
// assert.Equal(t, renderer.normalizeText(got), "[foo] bar")
|
|
||||||
// I had to comment it out because the unit tests keep failing and
|
|
||||||
// I don't know how to fix it. See more:
|
|
||||||
// https://github.com/gizak/termui/pull/22
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdownTextRenderer_NormalizedText(t *testing.T) {
|
|
||||||
renderer := MarkdownTextRenderer{"[ERROR](red,bold) Something went wrong"}
|
|
||||||
assert.Equal(t, renderer.NormalizedText(), "ERROR Something went wrong")
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertRenderSequence(t *testing.T, sequence RenderedSequence, last, background Attribute, text string, lenSequences int) bool {
|
|
||||||
msg := fmt.Sprintf("seq: %v", spew.Sdump(sequence))
|
|
||||||
assert.Equal(t, last, sequence.LastColor, msg)
|
|
||||||
assert.Equal(t, background, sequence.BackgroundColor, msg)
|
|
||||||
assert.Equal(t, text, sequence.NormalizedText, msg)
|
|
||||||
return assert.Equal(t, lenSequences, len(sequence.Sequences), msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertColorSubsequence(t *testing.T, s ColorSubsequence, color string, start, end int) {
|
|
||||||
assert.Equal(t, ColorSubsequence{StringToAttribute(color), start, end}, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdownTextRenderer_RenderSequence(t *testing.T) {
|
|
||||||
// Simple test.
|
|
||||||
renderer := MarkdownTextRenderer{"[ERROR](red,bold) something went wrong"}
|
|
||||||
got := renderer.RenderSequence(0, -1, 3, 5)
|
|
||||||
if assertRenderSequence(t, got, 3, 5, "ERROR something went wrong", 1) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
got = renderer.RenderSequence(3, 8, 3, 5)
|
|
||||||
if assertRenderSequence(t, got, 3, 5, "OR so", 1) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for mutiple colors.
|
|
||||||
renderer = MarkdownTextRenderer{"[foo](red) hello [bar](blue) world"}
|
|
||||||
got = renderer.RenderSequence(0, -1, 7, 2)
|
|
||||||
if assertRenderSequence(t, got, 7, 2, "foo hello bar world", 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 3)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "BLUE", 10, 13)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that out-of-bound color sequences are not added.
|
|
||||||
got = renderer.RenderSequence(4, 6, 8, 1)
|
|
||||||
assertRenderSequence(t, got, 8, 1, "he", 0)
|
|
||||||
|
|
||||||
// Test Half-rendered text
|
|
||||||
got = renderer.RenderSequence(1, 12, 0, 0)
|
|
||||||
if assertRenderSequence(t, got, 0, 0, "oo hello ba", 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 2)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "BLUE", 9, 11)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Half-rendered text (edges)
|
|
||||||
got = renderer.RenderSequence(2, 11, 0, 0)
|
|
||||||
if assertRenderSequence(t, got, 0, 0, "o hello b", 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "BLUE", 8, 9)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test barkets
|
|
||||||
|
|
||||||
// Test with unicodes
|
|
||||||
text := "笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺"
|
|
||||||
normalized := "笀耔 澉 灊灅甗 郔镺 笀耔 澉 灊灅甗 郔镺"
|
|
||||||
renderer = MarkdownTextRenderer{text}
|
|
||||||
got = renderer.RenderSequence(0, -1, 4, 7)
|
|
||||||
if assertRenderSequence(t, got, 4, 7, normalized, 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 3, 8)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "YELLOW", 17, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
got = renderer.RenderSequence(6, 7, 0, 0)
|
|
||||||
if assertRenderSequence(t, got, 0, 0, "灅", 1) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
got = renderer.RenderSequence(7, 19, 0, 0)
|
|
||||||
if assertRenderSequence(t, got, 0, 0, "甗 郔镺 笀耔 澉 灊灅", 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "YELLOW", 10, 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test inside
|
|
||||||
renderer = MarkdownTextRenderer{"foo [foobar](red) bar"}
|
|
||||||
got = renderer.RenderSequence(4, 10, 0, 0)
|
|
||||||
if assertRenderSequence(t, got, 0, 0, "foobar", 1) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED", 0, 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdownTextRenderer_Render(t *testing.T) {
|
|
||||||
renderer := MarkdownTextRenderer{"[foo](red,bold) [bar](blue)"}
|
|
||||||
got := renderer.Render(6, 8)
|
|
||||||
if assertRenderSequence(t, got, 6, 8, "foo bar", 2) {
|
|
||||||
assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 3)
|
|
||||||
assertColorSubsequence(t, got.Sequences[1], "blue", 4, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdownTextRendererFactory(t *testing.T) {
|
|
||||||
factory := MarkdownTextRendererFactory{}
|
|
||||||
expected := MarkdownTextRenderer{"Hello world"}
|
|
||||||
assert.Equal(t, factory.TextRenderer("Hello world"), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestColorSubsequencesToMap(t *testing.T) {
|
|
||||||
colorSubsequences := []ColorSubsequence{
|
|
||||||
{ColorRed, 1, 4},
|
|
||||||
{ColorBlue | AttrBold, 9, 10},
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := make(map[int]Attribute)
|
|
||||||
expected[1] = ColorRed
|
|
||||||
expected[2] = ColorRed
|
|
||||||
expected[3] = ColorRed
|
|
||||||
expected[9] = ColorBlue | AttrBold
|
|
||||||
|
|
||||||
assert.Equal(t, expected, ColorSubsequencesToMap(colorSubsequences))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTestRenderedSequence() RenderedSequence {
|
|
||||||
cs := []ColorSubsequence{
|
|
||||||
{ColorRed, 3, 5},
|
|
||||||
{ColorBlue | AttrBold, 9, 10},
|
|
||||||
}
|
|
||||||
|
|
||||||
return RenderedSequence{"Hello world", ColorWhite, ColorBlack, cs, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestPoint(char rune, x, y int, colorA ...Attribute) Point {
|
|
||||||
var color Attribute
|
|
||||||
if colorA != nil && len(colorA) == 1 {
|
|
||||||
color = colorA[0]
|
|
||||||
} else {
|
|
||||||
color = ColorWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
return Point{char, ColorBlack, color, x, y}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderedSequence_Buffer(t *testing.T) {
|
|
||||||
sequence := getTestRenderedSequence()
|
|
||||||
expected := []Point{
|
|
||||||
newTestPoint('H', 5, 7),
|
|
||||||
newTestPoint('e', 6, 7),
|
|
||||||
newTestPoint('l', 7, 7),
|
|
||||||
newTestPoint('l', 7, 7, ColorRed),
|
|
||||||
newTestPoint('o', 8, 7, ColorRed),
|
|
||||||
newTestPoint(' ', 9, 7),
|
|
||||||
newTestPoint('w', 10, 7),
|
|
||||||
newTestPoint('o', 11, 7),
|
|
||||||
newTestPoint('r', 12, 7),
|
|
||||||
newTestPoint('l', 13, 7, ColorBlue|AttrBold),
|
|
||||||
newTestPoint('d', 14, 7),
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer, lastColor := sequence.Buffer(5, 7)
|
|
||||||
|
|
||||||
assert.Equal(t, expected[:3], buffer[:3])
|
|
||||||
assert.Equal(t, ColorWhite, lastColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertPoint(t *testing.T, got Point, char rune, x, y int, colorA ...Attribute) {
|
|
||||||
expected := newTestPoint(char, x, y, colorA...)
|
|
||||||
assert.Equal(t, expected, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderedSequence_PointAt(t *testing.T) {
|
|
||||||
sequence := getTestRenderedSequence()
|
|
||||||
pointAt := func(n, x, y int) Point {
|
|
||||||
p, w := sequence.PointAt(n, x, y)
|
|
||||||
assert.Equal(t, w, 1)
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
AssertPoint(t, pointAt(0, 3, 4), 'H', 3, 4)
|
|
||||||
AssertPoint(t, pointAt(1, 2, 1), 'e', 2, 1)
|
|
||||||
AssertPoint(t, pointAt(2, 6, 3), 'l', 6, 3)
|
|
||||||
AssertPoint(t, pointAt(3, 8, 8), 'l', 8, 8, ColorRed)
|
|
||||||
AssertPoint(t, pointAt(4, 1, 4), 'o', 1, 4, ColorRed)
|
|
||||||
AssertPoint(t, pointAt(5, 3, 6), ' ', 3, 6)
|
|
||||||
AssertPoint(t, pointAt(6, 4, 3), 'w', 4, 3)
|
|
||||||
AssertPoint(t, pointAt(7, 5, 2), 'o', 5, 2)
|
|
||||||
AssertPoint(t, pointAt(8, 0, 5), 'r', 0, 5)
|
|
||||||
AssertPoint(t, pointAt(9, 9, 0), 'l', 9, 0, ColorBlue|AttrBold)
|
|
||||||
AssertPoint(t, pointAt(10, 7, 1), 'd', 7, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTestPlainRenderer() PlainRenderer {
|
|
||||||
return PlainRenderer{"[Hello](red) \x1b[31mworld"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlainRenderer_NormalizedText(t *testing.T) {
|
|
||||||
r := getTestPlainRenderer()
|
|
||||||
assert.Equal(t, "[Hello](red) \x1b[31mworld", r.NormalizedText())
|
|
||||||
assert.Equal(t, "[Hello](red) \x1b[31mworld", r.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlainRenderer_Render(t *testing.T) {
|
|
||||||
renderer := getTestPlainRenderer()
|
|
||||||
got := renderer.Render(5, 7)
|
|
||||||
assertRenderSequence(t, got, 5, 7, "[Hello](red) \x1b[31mworld", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlainRenderer_RenderSequence(t *testing.T) {
|
|
||||||
renderer := getTestPlainRenderer()
|
|
||||||
got := renderer.RenderSequence(3, 5, 9, 1)
|
|
||||||
assertRenderSequence(t, got, 9, 1, "ll", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlainRendererFactory(t *testing.T) {
|
|
||||||
factory := PlainRendererFactory{}
|
|
||||||
expected := PlainRenderer{"Hello world"}
|
|
||||||
assert.Equal(t, factory.TextRenderer("Hello world"), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPosUnicode(t *testing.T) {
|
|
||||||
// Every characters takes 3 bytes
|
|
||||||
text := "你好世界"
|
|
||||||
require.Equal(t, "你好", text[:6])
|
|
||||||
assert.Equal(t, 2, posUnicode(text, 6))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make `escapeCode` safe (i.e. replace \033 by \\033) so that it is not
|
|
||||||
// formatted.
|
|
||||||
// func makeEscapeCodeSafe(escapeCode string) string {
|
|
||||||
// return strings.Replace(escapeCode, "\033", "\\033", -1)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestEscapeCode_Color(t *testing.T) {
|
|
||||||
codes := map[EscapeCode]Attribute{
|
|
||||||
"\033[30m": ColorBlack,
|
|
||||||
"\033[31m": ColorRed,
|
|
||||||
"\033[32m": ColorGreen,
|
|
||||||
"\033[33m": ColorYellow,
|
|
||||||
"\033[34m": ColorBlue,
|
|
||||||
"\033[35m": ColorMagenta,
|
|
||||||
"\033[36m": ColorCyan,
|
|
||||||
"\033[37m": ColorWhite,
|
|
||||||
"\033[1;31m": ColorRed | AttrBold,
|
|
||||||
"\033[1;4;31m": ColorRed | AttrBold | AttrUnderline,
|
|
||||||
"\033[0m": ColorDefault,
|
|
||||||
}
|
|
||||||
|
|
||||||
for code, color := range codes {
|
|
||||||
got, err := code.Color()
|
|
||||||
msg := fmt.Sprintf("Escape code: '%v'", code.MakeSafe())
|
|
||||||
if assert.NoError(t, err, msg) {
|
|
||||||
assert.Equal(t, color, got, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidEscapeCodes := []EscapeCode{
|
|
||||||
"\03354m",
|
|
||||||
"[54m",
|
|
||||||
"\033[34",
|
|
||||||
"\033[34;m",
|
|
||||||
"\033[34m;",
|
|
||||||
"\033[34;",
|
|
||||||
"\033[5432m",
|
|
||||||
"t\033[30m",
|
|
||||||
"t\033[30ms",
|
|
||||||
"\033[30ms",
|
|
||||||
}
|
|
||||||
|
|
||||||
errMsg := "%v is not a valid ASCII escape code"
|
|
||||||
for _, invalidEscapeCode := range invalidEscapeCodes {
|
|
||||||
color, err := invalidEscapeCode.Color()
|
|
||||||
safeEscapeCode := invalidEscapeCode.MakeSafe()
|
|
||||||
expectedErr := fmt.Sprintf(errMsg, safeEscapeCode)
|
|
||||||
if assert.EqualError(t, err, expectedErr, "Expected: "+expectedErr) {
|
|
||||||
assert.Equal(t, color, Attribute(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outOfRangeCodes := []EscapeCode{
|
|
||||||
"\033[2m",
|
|
||||||
"\033[3m",
|
|
||||||
"\033[3m",
|
|
||||||
"\033[5m",
|
|
||||||
"\033[6m",
|
|
||||||
"\033[7m",
|
|
||||||
"\033[8m",
|
|
||||||
"\033[38m",
|
|
||||||
"\033[39m",
|
|
||||||
"\033[40m",
|
|
||||||
"\033[41m",
|
|
||||||
"\033[43m",
|
|
||||||
"\033[45m",
|
|
||||||
"\033[46m",
|
|
||||||
"\033[48m",
|
|
||||||
"\033[49m",
|
|
||||||
"\033[50m",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, code := range outOfRangeCodes {
|
|
||||||
color, err := code.Color()
|
|
||||||
safeCode := code.MakeSafe()
|
|
||||||
errMsg := fmt.Sprintf("Unkown/unsupported escape code: '%v'", safeCode)
|
|
||||||
if assert.EqualError(t, err, errMsg) {
|
|
||||||
assert.Equal(t, color, Attribute(0), "Escape Code: "+safeCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: check for out of slice panic on empty string
|
|
||||||
_, err := EscapeCode("").Color()
|
|
||||||
assert.EqualError(t, err, " is not a valid ASCII escape code")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCode_String(t *testing.T) {
|
|
||||||
e := EscapeCode("\033[32m")
|
|
||||||
assert.Equal(t, "\\033[32m", e.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCode_Raw(t *testing.T) {
|
|
||||||
e := EscapeCode("\033[32m")
|
|
||||||
assert.Equal(t, "\033[32m", e.Raw())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCodeRenderer_NormalizedText(t *testing.T) {
|
|
||||||
renderer := EscapeCodeRenderer{"\033[33mtest \033[35mfoo \033[33;1mbar"}
|
|
||||||
assert.Equal(t, "test foo bar", renderer.NormalizedText())
|
|
||||||
|
|
||||||
renderer = EscapeCodeRenderer{"hello \033[38mtest"}
|
|
||||||
assert.Equal(t, "hello \033[38mtest", renderer.NormalizedText())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCodeRenderer_RenderSequence(t *testing.T) {
|
|
||||||
black, white := ColorWhite, ColorBlack
|
|
||||||
renderer := EscapeCodeRenderer{"test \033[33mfoo \033[31mbar"}
|
|
||||||
sequence := renderer.RenderSequence(0, -1, black, white)
|
|
||||||
if assertRenderSequence(t, sequence, black, white, "test foo bar", 2) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 5, 9)
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 12)
|
|
||||||
getPoint := func(n int) Point {
|
|
||||||
point, width := sequence.PointAt(n, 10+n, 30)
|
|
||||||
assert.Equal(t, 1, width)
|
|
||||||
|
|
||||||
return point
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also test the points at to make sure that
|
|
||||||
// I didn't make a counting mistake...
|
|
||||||
AssertPoint(t, getPoint(0), 't', 10, 30)
|
|
||||||
AssertPoint(t, getPoint(1), 'e', 11, 30)
|
|
||||||
AssertPoint(t, getPoint(2), 's', 12, 30)
|
|
||||||
AssertPoint(t, getPoint(3), 't', 13, 30)
|
|
||||||
AssertPoint(t, getPoint(4), ' ', 14, 30)
|
|
||||||
AssertPoint(t, getPoint(5), 'f', 15, 30, ColorYellow)
|
|
||||||
AssertPoint(t, getPoint(6), 'o', 16, 30, ColorYellow)
|
|
||||||
AssertPoint(t, getPoint(7), 'o', 17, 30, ColorYellow)
|
|
||||||
AssertPoint(t, getPoint(8), ' ', 18, 30, ColorYellow)
|
|
||||||
AssertPoint(t, getPoint(9), 'b', 19, 30, ColorRed)
|
|
||||||
AssertPoint(t, getPoint(10), 'a', 20, 30, ColorRed)
|
|
||||||
AssertPoint(t, getPoint(11), 'r', 21, 30, ColorRed)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer = EscapeCodeRenderer{"甗 郔\033[33m镺 笀耔 澉 灊\033[31m灅甗"}
|
|
||||||
sequence = renderer.RenderSequence(2, -1, black, white)
|
|
||||||
if assertRenderSequence(t, sequence, black, white, "郔镺 笀耔 澉 灊灅甗", 2) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 1, 9)
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 11)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer = EscapeCodeRenderer{"\033[33mHell\033[31mo world"}
|
|
||||||
sequence = renderer.RenderSequence(2, -1, black, white)
|
|
||||||
if assertRenderSequence(t, sequence, black, white, "llo world", 2) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 0, 2)
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[1], "RED", 2, 9)
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence = renderer.RenderSequence(1, 7, black, white)
|
|
||||||
if assertRenderSequence(t, sequence, black, white, "ello w", 2) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 0, 3)
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[1], "RED", 3, 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence = renderer.RenderSequence(6, 10, black, white)
|
|
||||||
if assertRenderSequence(t, sequence, black, white, "worl", 1) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "RED", 0, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with out-of-range escape code
|
|
||||||
renderer = EscapeCodeRenderer{"hello \033[38mtest"}
|
|
||||||
sequence = renderer.RenderSequence(0, -1, black, white)
|
|
||||||
assertRenderSequence(t, sequence, black, white, "hello \033[38mtest", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCodeRenderer_Render(t *testing.T) {
|
|
||||||
renderer := EscapeCodeRenderer{"test \033[33mfoo \033[31mbar"}
|
|
||||||
sequence := renderer.Render(4, 6)
|
|
||||||
if assertRenderSequence(t, sequence, 4, 6, "test foo bar", 2) {
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[0], "YELLOW", 5, 9)
|
|
||||||
assertColorSubsequence(t, sequence.Sequences[1], "RED", 9, 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeCodeRendererFactory_TextRenderer(t *testing.T) {
|
|
||||||
factory := EscapeCodeRendererFactory{}
|
|
||||||
assert.Equal(t, EscapeCodeRenderer{"foo"}, factory.TextRenderer("foo"))
|
|
||||||
assert.Equal(t, EscapeCodeRenderer{"bar"}, factory.TextRenderer("bar"))
|
|
||||||
}
|
|
209
textbuilder.go
Normal file
209
textbuilder.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextBuilder is a minial interface to produce text []Cell using sepcific syntax (markdown).
|
||||||
|
type TextBuilder interface {
|
||||||
|
Build(s string, fg, bg Attribute) []Cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
|
||||||
|
type MarkdownTxBuilder struct {
|
||||||
|
baseFg Attribute
|
||||||
|
baseBg Attribute
|
||||||
|
plainTx []rune
|
||||||
|
markers []marker
|
||||||
|
}
|
||||||
|
|
||||||
|
type marker struct {
|
||||||
|
st int
|
||||||
|
ed int
|
||||||
|
fg Attribute
|
||||||
|
bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorMap = map[string]Attribute{
|
||||||
|
"red": ColorRed,
|
||||||
|
"blue": ColorBlue,
|
||||||
|
"black": ColorBlack,
|
||||||
|
"cyan": ColorCyan,
|
||||||
|
"white": ColorWhite,
|
||||||
|
"default": ColorDefault,
|
||||||
|
"green": ColorGreen,
|
||||||
|
"magenta": ColorMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
var attrMap = map[string]Attribute{
|
||||||
|
"bold": AttrBold,
|
||||||
|
"underline": AttrUnderline,
|
||||||
|
"reverse": AttrReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
func rmSpc(s string) string {
|
||||||
|
reg := regexp.MustCompile(`\s+`)
|
||||||
|
return reg.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
|
||||||
|
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
|
||||||
|
fg := mtb.baseFg
|
||||||
|
bg := mtb.baseBg
|
||||||
|
|
||||||
|
updateAttr := func(a Attribute, attrs []string) Attribute {
|
||||||
|
for _, s := range attrs {
|
||||||
|
if c, ok := colorMap[s]; ok {
|
||||||
|
a &= 0xFF00 //erase clr 0 ~ 8 bits
|
||||||
|
a |= c // set clr
|
||||||
|
}
|
||||||
|
if c, ok := attrMap[s]; ok {
|
||||||
|
a |= c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := strings.Split(s, ",")
|
||||||
|
fgs := []string{}
|
||||||
|
bgs := []string{}
|
||||||
|
for _, v := range ss {
|
||||||
|
subs := strings.Split(v, "-")
|
||||||
|
if len(subs) > 1 {
|
||||||
|
if subs[0] == "fg" {
|
||||||
|
fgs = append(fgs, subs[1])
|
||||||
|
}
|
||||||
|
if subs[0] == "bg" {
|
||||||
|
bgs = append(bgs, subs[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fg = updateAttr(fg, fgs)
|
||||||
|
bg = updateAttr(bg, bgs)
|
||||||
|
return fg, bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mtb *MarkdownTxBuilder) reset() {
|
||||||
|
mtb.plainTx = []rune{}
|
||||||
|
mtb.markers = []marker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse
|
||||||
|
func (mtb *MarkdownTxBuilder) parse(str string) {
|
||||||
|
rs := str2runes(str)
|
||||||
|
normTx := []rune{}
|
||||||
|
square := []rune{}
|
||||||
|
brackt := []rune{}
|
||||||
|
accSquare := false
|
||||||
|
accBrackt := false
|
||||||
|
cntSquare := 0
|
||||||
|
|
||||||
|
reset := func() {
|
||||||
|
square = []rune{}
|
||||||
|
brackt = []rune{}
|
||||||
|
accSquare = false
|
||||||
|
accBrackt = false
|
||||||
|
cntSquare = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback := func() {
|
||||||
|
normTx = append(normTx, square...)
|
||||||
|
normTx = append(normTx, brackt...)
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
chop := func(s []rune) []rune {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Println(string(s))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range rs {
|
||||||
|
switch {
|
||||||
|
// stacking brackt
|
||||||
|
case accBrackt:
|
||||||
|
brackt = append(brackt, r)
|
||||||
|
if ')' == r {
|
||||||
|
fg, bg := mtb.readAttr(string(chop(brackt)))
|
||||||
|
st := len(normTx)
|
||||||
|
ed := len(normTx) + len(square) - 2
|
||||||
|
mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
|
||||||
|
normTx = append(normTx, chop(square)...)
|
||||||
|
reset()
|
||||||
|
} else if i+1 == len(rs) {
|
||||||
|
rollback()
|
||||||
|
}
|
||||||
|
// stacking square
|
||||||
|
case accSquare:
|
||||||
|
switch {
|
||||||
|
// squares closed and followed by a '('
|
||||||
|
case cntSquare == 0 && '(' == r:
|
||||||
|
accBrackt = true
|
||||||
|
brackt = append(brackt, '(')
|
||||||
|
// squares closed but not followed by a '('
|
||||||
|
case cntSquare == 0:
|
||||||
|
rollback()
|
||||||
|
if '[' == r {
|
||||||
|
accSquare = true
|
||||||
|
cntSquare = 1
|
||||||
|
brackt = append(brackt, '[')
|
||||||
|
} else {
|
||||||
|
normTx = append(normTx, r)
|
||||||
|
}
|
||||||
|
// hit the end
|
||||||
|
case i+1 == len(rs):
|
||||||
|
square = append(square, r)
|
||||||
|
rollback()
|
||||||
|
case '[' == r:
|
||||||
|
cntSquare++
|
||||||
|
square = append(square, '[')
|
||||||
|
case ']' == r:
|
||||||
|
cntSquare--
|
||||||
|
square = append(square, ']')
|
||||||
|
// normal char
|
||||||
|
default:
|
||||||
|
square = append(square, r)
|
||||||
|
}
|
||||||
|
// stacking normTx
|
||||||
|
default:
|
||||||
|
if '[' == r {
|
||||||
|
accSquare = true
|
||||||
|
cntSquare = 1
|
||||||
|
square = append(square, '[')
|
||||||
|
} else {
|
||||||
|
normTx = append(normTx, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtb.plainTx = normTx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
|
||||||
|
mtb.baseFg = fg
|
||||||
|
mtb.baseBg = bg
|
||||||
|
mtb.reset()
|
||||||
|
mtb.parse(s)
|
||||||
|
cs := make([]Cell, len(mtb.plainTx))
|
||||||
|
for i := range cs {
|
||||||
|
cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
|
||||||
|
}
|
||||||
|
for _, mrk := range mtb.markers {
|
||||||
|
for i := mrk.st; i < mrk.ed; i++ {
|
||||||
|
cs[i].Fg = mrk.fg
|
||||||
|
cs[i].Bg = mrk.bg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarkdownTxBuilder() TextBuilder {
|
||||||
|
return MarkdownTxBuilder{}
|
||||||
|
}
|
66
textbuilder_test.go
Normal file
66
textbuilder_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestReadAttr(t *testing.T) {
|
||||||
|
m := MarkdownTxBuilder{}
|
||||||
|
m.baseFg = ColorCyan | AttrUnderline
|
||||||
|
m.baseBg = ColorBlue | AttrBold
|
||||||
|
fg, bg := m.readAttr("fg-red,bg-reverse")
|
||||||
|
if fg != ColorRed|AttrUnderline || bg != ColorBlue|AttrBold|AttrReverse {
|
||||||
|
t.Error("readAttr failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMTBParse(t *testing.T) {
|
||||||
|
/*
|
||||||
|
str := func(cs []Cell) string {
|
||||||
|
rs := make([]rune, len(cs))
|
||||||
|
for i := range cs {
|
||||||
|
rs[i] = cs[i].Ch
|
||||||
|
}
|
||||||
|
return string(rs)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
tbls := [][]string{
|
||||||
|
{"hello world", "hello world"},
|
||||||
|
{"[hello](fg-red) world", "hello world"},
|
||||||
|
{"[[hello]](bg-red) world", "[hello] world"},
|
||||||
|
{"[1] hello world", "[1] hello world"},
|
||||||
|
{"[[1]](bg-white) [hello] world", "[1] [hello] world"},
|
||||||
|
{"[hello world]", "[hello world]"},
|
||||||
|
{"", ""},
|
||||||
|
{"[hello world)", "[hello world)"},
|
||||||
|
{"[0] [hello](bg-red)[ world](fg-blue)!", "[0] hello world!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := MarkdownTxBuilder{}
|
||||||
|
m.baseFg = ColorWhite
|
||||||
|
m.baseBg = ColorDefault
|
||||||
|
for _, s := range tbls {
|
||||||
|
m.reset()
|
||||||
|
m.parse(s[0])
|
||||||
|
res := string(m.plainTx)
|
||||||
|
if s[1] != res {
|
||||||
|
t.Errorf("\ninput :%s\nshould:%s\noutput:%s", s[0], s[1], res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.reset()
|
||||||
|
m.parse("[0] [hello](bg-red)[ world](fg-blue)")
|
||||||
|
if len(m.markers) != 2 &&
|
||||||
|
m.markers[0].st == 4 &&
|
||||||
|
m.markers[0].ed == 11 &&
|
||||||
|
m.markers[0].fg == ColorWhite &&
|
||||||
|
m.markers[0].bg == ColorRed {
|
||||||
|
t.Error("markers dismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
m2 := NewMarkdownTxBuilder()
|
||||||
|
cs := m2.Build("[0] [hellob-e) wrd]fgblue)!", ColorWhite, ColorBlack)
|
||||||
|
cs = m2.Build("[0] [hello](bg-red) [world](fg-blue)!", ColorWhite, ColorBlack)
|
||||||
|
if cs[4].Ch != 'h' && cs[4].Bg != ColorRed && cs[4].Fg != ColorWhite {
|
||||||
|
t.Error("dismatch in Build")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user