aboutsummaryrefslogblamecommitdiffstats
path: root/vendor/github.com/gizak/termui/textbuilder.go
blob: 12e2055b5095f4550187eed21bf7f8b1cbf02bc2 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                       







                                                                

                                          

 
                                                                                              


















































































































































































                                                                                         




























































                                                                                                    























                                                                        
// 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{}
}