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