279 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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{}
 | 
						|
}
 |