Merge from Matt3o12/colored-list
This commit is contained in:
commit
00192cd7b0
@ -112,6 +112,11 @@ The `helloworld` color scheme drops in some colors!
|
|||||||
|
|
||||||
<img src="./example/list.png" alt="list" type="image/png" width="200">
|
<img src="./example/list.png" alt="list" type="image/png" width="200">
|
||||||
|
|
||||||
|
#### Colored List
|
||||||
|
[demo code](https://github.com/gizak/termui/blob/master/example/coloredList.go)
|
||||||
|
|
||||||
|
TODO: Image (let's wait until the implementation is finished).
|
||||||
|
|
||||||
#### Gauge
|
#### Gauge
|
||||||
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go)
|
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go)
|
||||||
|
|
||||||
|
90
example/coloredList.go
Normal file
90
example/coloredList.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/gizak/termui"
|
||||||
|
import "github.com/nsf/termbox-go"
|
||||||
|
|
||||||
|
func markdownList() *termui.List {
|
||||||
|
strs := []string{
|
||||||
|
"[0] github.com/gizak/termui",
|
||||||
|
"[1] 笀耔 [澉 灊灅甗](RED) 郔镺 笀耔 澉 [灊灅甗](yellow) 郔镺",
|
||||||
|
"[2] こんにちは世界",
|
||||||
|
"[3] keyboard.go",
|
||||||
|
"[4] [output](RED).go",
|
||||||
|
"[5] random_out.go",
|
||||||
|
"[6] [dashboard](BOLD).go",
|
||||||
|
"[7] nsf/termbox-go",
|
||||||
|
"[8] OVERFLOW!!!!!!!!!"}
|
||||||
|
|
||||||
|
list := termui.NewList()
|
||||||
|
list.Items = strs
|
||||||
|
list.Height = 15
|
||||||
|
list.Width = 26
|
||||||
|
list.RendererFactory = termui.MarkdownTextRendererFactory{}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideList(list *termui.List) *termui.List {
|
||||||
|
list.Border.Label = "List - Hidden"
|
||||||
|
list.Overflow = "hidden"
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapList(list *termui.List) *termui.List {
|
||||||
|
list.Border.Label = "List - Wrapped"
|
||||||
|
list.Overflow = "wrap"
|
||||||
|
list.X = 30
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeList() *termui.List {
|
||||||
|
strs := []string{
|
||||||
|
"[0] github.com/gizak/termui",
|
||||||
|
"[1] 笀耔 \033[31m澉 灊灅甗 \033[0m郔镺 笀耔 澉 \033[33m灊灅甗 郔镺",
|
||||||
|
"[2] こんにちは世界",
|
||||||
|
"[3] keyboard.go",
|
||||||
|
"[4] \033[31moutput\033[0m.go",
|
||||||
|
"[5] random_out.go",
|
||||||
|
"[6] \033[1mdashboard\033[0m.go",
|
||||||
|
"[7] nsf/termbox-go",
|
||||||
|
"[8] OVERFLOW!!!!!!!\033[31;1m!!!!!!!!!!!!\033[0m!!!",
|
||||||
|
}
|
||||||
|
|
||||||
|
list := termui.NewList()
|
||||||
|
list.RendererFactory = termui.EscapeCodeRendererFactory{}
|
||||||
|
list.Items = strs
|
||||||
|
list.Height = 15
|
||||||
|
list.Width = 26
|
||||||
|
list.Y = 15
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := termui.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer termui.Close()
|
||||||
|
|
||||||
|
hiddenMarkdownList := hideList(markdownList())
|
||||||
|
wrappedMarkdownList := wrapList(markdownList())
|
||||||
|
|
||||||
|
hiddenEscapeList := hideList(escapeList())
|
||||||
|
wrappedEscapeList := wrapList(escapeList())
|
||||||
|
|
||||||
|
lists := []termui.Bufferer{
|
||||||
|
hiddenEscapeList,
|
||||||
|
hiddenMarkdownList,
|
||||||
|
wrappedMarkdownList,
|
||||||
|
wrappedEscapeList,
|
||||||
|
}
|
||||||
|
|
||||||
|
termui.UseTheme("helloworld")
|
||||||
|
termui.Render(lists...)
|
||||||
|
termbox.PollEvent()
|
||||||
|
}
|
@ -29,7 +29,8 @@ func main() {
|
|||||||
par1.X = 20
|
par1.X = 20
|
||||||
par1.Border.Label = "标签"
|
par1.Border.Label = "标签"
|
||||||
|
|
||||||
par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically")
|
par2 := termui.NewPar("Simple colored text\nwith label. It [can be](RED) multilined with \\n or [break automatically](GREEN, BOLD)")
|
||||||
|
par2.RendererFactory = termui.MarkdownTextRendererFactory{}
|
||||||
par2.Height = 5
|
par2.Height = 5
|
||||||
par2.Width = 37
|
par2.Width = 37
|
||||||
par2.Y = 4
|
par2.Y = 4
|
||||||
|
89
helper.go
89
helper.go
@ -4,7 +4,12 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
import tm "github.com/nsf/termbox-go"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
import rw "github.com/mattn/go-runewidth"
|
import rw "github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
/* ---------------Port from termbox-go --------------------- */
|
/* ---------------Port from termbox-go --------------------- */
|
||||||
@ -45,15 +50,39 @@ func str2runes(s string) []rune {
|
|||||||
return []rune(s)
|
return []rune(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here for backwards-compatibility.
|
||||||
func trimStr2Runes(s string, w int) []rune {
|
func trimStr2Runes(s string, w int) []rune {
|
||||||
|
return TrimStr2Runes(s, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
|
||||||
|
// of that string if string is grather then n. If string is small then w,
|
||||||
|
// return the runes.
|
||||||
|
func TrimStr2Runes(s string, w int) []rune {
|
||||||
if w <= 0 {
|
if w <= 0 {
|
||||||
return []rune{}
|
return []rune{}
|
||||||
}
|
}
|
||||||
|
|
||||||
sw := rw.StringWidth(s)
|
sw := rw.StringWidth(s)
|
||||||
if sw > w {
|
if sw > w {
|
||||||
return []rune(rw.Truncate(s, w, dot))
|
return []rune(rw.Truncate(s, w, dot))
|
||||||
}
|
}
|
||||||
return str2runes(s) //[]rune(rw.Truncate(s, w, ""))
|
return str2runes(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStrIfAppropriate trim string to "s[:-1] + …"
|
||||||
|
// if string > width otherwise return string
|
||||||
|
func TrimStrIfAppropriate(s string, w int) string {
|
||||||
|
if w <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sw := rw.StringWidth(s)
|
||||||
|
if sw > w {
|
||||||
|
return rw.Truncate(s, w, dot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func strWidth(s string) int {
|
func strWidth(s string) int {
|
||||||
@ -63,3 +92,59 @@ func strWidth(s string) int {
|
|||||||
func charWidth(ch rune) int {
|
func charWidth(ch rune) int {
|
||||||
return rw.RuneWidth(ch)
|
return rw.RuneWidth(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var whiteSpaceRegex = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
|
// StringToAttribute converts text to a termui attribute. You may specifiy more
|
||||||
|
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
|
||||||
|
// are ignored.
|
||||||
|
func StringToAttribute(text string) Attribute {
|
||||||
|
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
|
||||||
|
attributes := strings.Split(text, ",")
|
||||||
|
result := Attribute(0)
|
||||||
|
|
||||||
|
for _, theAttribute := range attributes {
|
||||||
|
var match Attribute
|
||||||
|
switch theAttribute {
|
||||||
|
case "reset", "default":
|
||||||
|
match = ColorDefault
|
||||||
|
|
||||||
|
case "black":
|
||||||
|
match = ColorBlack
|
||||||
|
|
||||||
|
case "red":
|
||||||
|
match = ColorRed
|
||||||
|
|
||||||
|
case "green":
|
||||||
|
match = ColorGreen
|
||||||
|
|
||||||
|
case "yellow":
|
||||||
|
match = ColorYellow
|
||||||
|
|
||||||
|
case "blue":
|
||||||
|
match = ColorBlue
|
||||||
|
|
||||||
|
case "magenta":
|
||||||
|
match = ColorMagenta
|
||||||
|
|
||||||
|
case "cyan":
|
||||||
|
match = ColorCyan
|
||||||
|
|
||||||
|
case "white":
|
||||||
|
match = ColorWhite
|
||||||
|
|
||||||
|
case "bold":
|
||||||
|
match = AttrBold
|
||||||
|
|
||||||
|
case "underline":
|
||||||
|
match = AttrUnderline
|
||||||
|
|
||||||
|
case "reverse":
|
||||||
|
match = AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
result |= match
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -7,22 +7,20 @@ package termui
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStr2Rune(t *testing.T) {
|
func TestStr2Rune(t *testing.T) {
|
||||||
s := "你好,世界."
|
s := "你好,世界."
|
||||||
rs := str2runes(s)
|
rs := str2runes(s)
|
||||||
if len(rs) != 6 {
|
if len(rs) != 6 {
|
||||||
t.Error()
|
t.Error(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWidth(t *testing.T) {
|
func TestWidth(t *testing.T) {
|
||||||
s0 := "つのだ☆HIRO"
|
s0 := "つのだ☆HIRO"
|
||||||
s1 := "11111111111"
|
s1 := "11111111111"
|
||||||
spew.Dump(s0)
|
|
||||||
spew.Dump(s1)
|
|
||||||
// above not align for setting East Asian Ambiguous to wide!!
|
// above not align for setting East Asian Ambiguous to wide!!
|
||||||
|
|
||||||
if strWidth(s0) != strWidth(s1) {
|
if strWidth(s0) != strWidth(s1) {
|
||||||
@ -56,3 +54,17 @@ func TestTrim(t *testing.T) {
|
|||||||
t.Error("avoid trim failed")
|
t.Error("avoid trim failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
|
||||||
|
assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimStrIfAppropriate(t *testing.T) {
|
||||||
|
assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
|
||||||
|
assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToAttribute(t *testing.T) {
|
||||||
|
assert.Equal(t, ColorRed, StringToAttribute("ReD"))
|
||||||
|
assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
|
||||||
|
}
|
||||||
|
97
list.go
97
list.go
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
package termui
|
package termui
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// List displays []string as its items,
|
// List displays []string as its items,
|
||||||
// it has a Overflow option (default is "hidden"), when set to "hidden",
|
// it has a Overflow option (default is "hidden"), when set to "hidden",
|
||||||
// the item exceeding List's width is truncated, but when set to "wrap",
|
// the item exceeding List's width is truncated, but when set to "wrap",
|
||||||
@ -31,10 +29,11 @@ import "strings"
|
|||||||
*/
|
*/
|
||||||
type List struct {
|
type List struct {
|
||||||
Block
|
Block
|
||||||
Items []string
|
Items []string
|
||||||
Overflow string
|
Overflow string
|
||||||
ItemFgColor Attribute
|
ItemFgColor Attribute
|
||||||
ItemBgColor Attribute
|
ItemBgColor Attribute
|
||||||
|
RendererFactory TextRendererFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewList returns a new *List with current theme.
|
// NewList returns a new *List with current theme.
|
||||||
@ -43,62 +42,54 @@ func NewList() *List {
|
|||||||
l.Overflow = "hidden"
|
l.Overflow = "hidden"
|
||||||
l.ItemFgColor = theme.ListItemFg
|
l.ItemFgColor = theme.ListItemFg
|
||||||
l.ItemBgColor = theme.ListItemBg
|
l.ItemBgColor = theme.ListItemBg
|
||||||
|
l.RendererFactory = PlainRendererFactory{}
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
// Buffer implements Bufferer interface.
|
||||||
func (l *List) Buffer() []Point {
|
func (l *List) Buffer() []Point {
|
||||||
ps := l.Block.Buffer()
|
buffer := l.Block.Buffer()
|
||||||
switch l.Overflow {
|
|
||||||
case "wrap":
|
breakLoop := func(y int) bool {
|
||||||
rs := str2runes(strings.Join(l.Items, "\n"))
|
return y+1 > l.innerHeight
|
||||||
i, j, k := 0, 0, 0
|
}
|
||||||
for i < l.innerHeight && k < len(rs) {
|
y := 0
|
||||||
w := charWidth(rs[k])
|
|
||||||
if rs[k] == '\n' || j+w > l.innerWidth {
|
MainLoop:
|
||||||
i++
|
for _, item := range l.Items {
|
||||||
j = 0
|
x := 0
|
||||||
if rs[k] == '\n' {
|
bg, fg := l.ItemFgColor, l.ItemBgColor
|
||||||
k++
|
renderer := l.RendererFactory.TextRenderer(item)
|
||||||
|
sequence := renderer.Render(bg, fg)
|
||||||
|
|
||||||
|
for n := range []rune(sequence.NormalizedText) {
|
||||||
|
point, width := sequence.PointAt(n, x+l.innerX, y+l.innerY)
|
||||||
|
|
||||||
|
if width+x <= l.innerWidth {
|
||||||
|
buffer = append(buffer, point)
|
||||||
|
x += width
|
||||||
|
} else {
|
||||||
|
if l.Overflow == "wrap" {
|
||||||
|
y++
|
||||||
|
if breakLoop(y) {
|
||||||
|
break MainLoop
|
||||||
|
}
|
||||||
|
x = 0
|
||||||
|
} else {
|
||||||
|
dotR := []rune(dot)[0]
|
||||||
|
dotX := l.innerWidth + l.innerX - charWidth(dotR)
|
||||||
|
p := newPointWithAttrs(dotR, dotX, y+l.innerY, bg, fg)
|
||||||
|
buffer = append(buffer, p)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
pi := Point{}
|
|
||||||
pi.X = l.innerX + j
|
|
||||||
pi.Y = l.innerY + i
|
|
||||||
|
|
||||||
pi.Ch = rs[k]
|
|
||||||
pi.Bg = l.ItemBgColor
|
|
||||||
pi.Fg = l.ItemFgColor
|
|
||||||
|
|
||||||
ps = append(ps, pi)
|
|
||||||
k++
|
|
||||||
j++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "hidden":
|
y++
|
||||||
trimItems := l.Items
|
if breakLoop(y) {
|
||||||
if len(trimItems) > l.innerHeight {
|
break MainLoop
|
||||||
trimItems = trimItems[:l.innerHeight]
|
|
||||||
}
|
|
||||||
for i, v := range trimItems {
|
|
||||||
rs := trimStr2Runes(v, l.innerWidth)
|
|
||||||
|
|
||||||
j := 0
|
|
||||||
for _, vv := range rs {
|
|
||||||
w := charWidth(vv)
|
|
||||||
p := Point{}
|
|
||||||
p.X = l.innerX + j
|
|
||||||
p.Y = l.innerY + i
|
|
||||||
|
|
||||||
p.Ch = vv
|
|
||||||
p.Bg = l.ItemBgColor
|
|
||||||
p.Fg = l.ItemFgColor
|
|
||||||
|
|
||||||
ps = append(ps, p)
|
|
||||||
j += w
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return l.Block.chopOverflow(ps)
|
|
||||||
|
return l.Block.chopOverflow(buffer)
|
||||||
}
|
}
|
||||||
|
56
p.go
56
p.go
@ -13,38 +13,43 @@ package termui
|
|||||||
*/
|
*/
|
||||||
type Par struct {
|
type Par struct {
|
||||||
Block
|
Block
|
||||||
Text string
|
Text string
|
||||||
TextFgColor Attribute
|
TextFgColor Attribute
|
||||||
TextBgColor Attribute
|
TextBgColor Attribute
|
||||||
|
RendererFactory TextRendererFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPar returns a new *Par with given text as its content.
|
// NewPar returns a new *Par with given text as its content.
|
||||||
func NewPar(s string) *Par {
|
func NewPar(s string) *Par {
|
||||||
return &Par{
|
return &Par{
|
||||||
Block: *NewBlock(),
|
Block: *NewBlock(),
|
||||||
Text: s,
|
Text: s,
|
||||||
TextFgColor: theme.ParTextFg,
|
TextFgColor: theme.ParTextFg,
|
||||||
TextBgColor: theme.ParTextBg}
|
TextBgColor: theme.ParTextBg,
|
||||||
|
RendererFactory: PlainRendererFactory{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer implements Bufferer interface.
|
// Buffer implements Bufferer interface.
|
||||||
func (p *Par) Buffer() []Point {
|
func (p *Par) Buffer() []Point {
|
||||||
ps := p.Block.Buffer()
|
ps := p.Block.Buffer()
|
||||||
|
|
||||||
rs := str2runes(p.Text)
|
fg, bg := p.TextFgColor, p.TextBgColor
|
||||||
i, j, k := 0, 0, 0
|
sequence := p.RendererFactory.TextRenderer(p.Text).Render(fg, bg)
|
||||||
for i < p.innerHeight && k < len(rs) {
|
runes := []rune(sequence.NormalizedText)
|
||||||
// the width of char is about to print
|
|
||||||
w := charWidth(rs[k])
|
|
||||||
|
|
||||||
if rs[k] == '\n' || j+w > p.innerWidth {
|
y, x, n := 0, 0, 0
|
||||||
i++
|
for y < p.innerHeight && n < len(runes) {
|
||||||
j = 0 // set x = 0
|
point, width := sequence.PointAt(n, x+p.innerX, y+p.innerY)
|
||||||
if rs[k] == '\n' {
|
|
||||||
k++
|
if runes[n] == '\n' || x+width > p.innerWidth {
|
||||||
|
y++
|
||||||
|
x = 0 // set x = 0
|
||||||
|
if runes[n] == '\n' {
|
||||||
|
n++
|
||||||
}
|
}
|
||||||
|
|
||||||
if i >= p.innerHeight {
|
if y >= p.innerHeight {
|
||||||
ps = append(ps, newPointWithAttrs('…',
|
ps = append(ps, newPointWithAttrs('…',
|
||||||
p.innerX+p.innerWidth-1,
|
p.innerX+p.innerWidth-1,
|
||||||
p.innerY+p.innerHeight-1,
|
p.innerY+p.innerHeight-1,
|
||||||
@ -54,18 +59,11 @@ func (p *Par) Buffer() []Point {
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pi := Point{}
|
|
||||||
pi.X = p.innerX + j
|
|
||||||
pi.Y = p.innerY + i
|
|
||||||
|
|
||||||
pi.Ch = rs[k]
|
ps = append(ps, point)
|
||||||
pi.Bg = p.TextBgColor
|
n++
|
||||||
pi.Fg = p.TextFgColor
|
x += width
|
||||||
|
|
||||||
ps = append(ps, pi)
|
|
||||||
|
|
||||||
k++
|
|
||||||
j += w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.Block.chopOverflow(ps)
|
return p.Block.chopOverflow(ps)
|
||||||
}
|
}
|
||||||
|
454
textRender.go
Normal file
454
textRender.go
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
454
textRender_test.go
Normal file
454
textRender_test.go
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
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"))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user