32b8d8e68b
'yellow' wasn't included in the colorMap used by TextBuilder, making it impossible to specify that very handy color from within text.
212 lines
4.4 KiB
Go
212 lines
4.4 KiB
Go
package termui
|
|
|
|
import (
|
|
"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
|
|
}
|
|
|
|
// DefaultTxBuilder is set to be MarkdownTxBuilder.
|
|
var DefaultTxBuilder = NewMarkdownTxBuilder()
|
|
|
|
// 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,
|
|
"yellow": ColorYellow,
|
|
"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 {
|
|
// replace the color
|
|
if c, ok := colorMap[s]; ok {
|
|
a &= 0xFF00 // erase clr 0 ~ 8 bits
|
|
a |= c // set clr
|
|
}
|
|
// add attrs
|
|
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 streams and parses text into normalized text and render sequence.
|
|
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
|
|
}
|
|
// pipe stacks into normTx and clear
|
|
rollback := func() {
|
|
normTx = append(normTx, square...)
|
|
normTx = append(normTx, brackt...)
|
|
reset()
|
|
}
|
|
// chop first and last
|
|
chop := func(s []rune) []rune {
|
|
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
|
|
}
|
|
|
|
// Build implements TextBuilder interface.
|
|
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
|
|
}
|
|
|
|
// NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
|
|
func NewMarkdownTxBuilder() TextBuilder {
|
|
return MarkdownTxBuilder{}
|
|
}
|