// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"regexp"
"strings"
"github.com/mitchellh/go-wordwrap"
)
// TextBuilder is a minimal interface to produce text []Cell using specific 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
}
func wrapTx(cs []Cell, wl int) []Cell {
tmpCell := make([]Cell, len(cs))
copy(tmpCell, cs)
// get the plaintext
plain := CellsToStr(cs)
// wrap
plainWrapped := wordwrap.WrapString(plain, uint(wl))
// find differences and insert
finalCell := tmpCell // finalcell will get the inserts and is what is returned
plainRune := []rune(plain)
plainWrappedRune := []rune(plainWrapped)
trigger := "go"
plainRuneNew := plainRune
for trigger != "stop" {
plainRune = plainRuneNew
for i := range plainRune {
if plainRune[i] == plainWrappedRune[i] {
trigger = "stop"
} else if plainRune[i] != plainWrappedRune[i] && plainWrappedRune[i] == 10 {
trigger = "go"
cell := Cell{10, 0, 0}
j := i - 0
// insert a cell into the []Cell in correct position
tmpCell[i] = cell
// insert the newline into plain so we avoid indexing errors
plainRuneNew = append(plainRune, 10)
copy(plainRuneNew[j+1:], plainRuneNew[j:])
plainRuneNew[j] = plainWrappedRune[j]
// restart the inner for loop until plain and plain wrapped are
// the same; yeah, it's inefficient, but the text amounts
// should be small
break
} else if plainRune[i] != plainWrappedRune[i] &&
plainWrappedRune[i-1] == 10 && // if the prior rune is a newline
plainRune[i] == 32 { // and this rune is a space
trigger = "go"
// need to delete plainRune[i] because it gets rid of an extra
// space
plainRuneNew = append(plainRune[:i], plainRune[i+1:]...)
break
} else {
trigger = "stop" // stops the outer for loop
}
}
}
finalCell = tmpCell
return finalCell
}
// 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{}
}