mirror of
https://github.com/gizak/termui.git
synced 2025-04-26 13:48:54 +08:00
Merge a7d7cdbff3eaaa54d3f7002703be9959076813bb into 2b8f0c7960e9553acea6d579a740713066da5e13
This commit is contained in:
commit
d63aa965f5
200
style_parser.go
200
style_parser.go
@ -21,10 +21,17 @@ const (
|
|||||||
|
|
||||||
tokenBeginStyle = '('
|
tokenBeginStyle = '('
|
||||||
tokenEndStyle = ')'
|
tokenEndStyle = ')'
|
||||||
|
|
||||||
|
tokenStyleKey = "]("
|
||||||
)
|
)
|
||||||
|
|
||||||
type parserState uint
|
type parserState uint
|
||||||
|
|
||||||
|
type StyleBlock struct {
|
||||||
|
Start int
|
||||||
|
End int
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
parserStateDefault parserState = iota
|
parserStateDefault parserState = iota
|
||||||
parserStateStyleItems
|
parserStateStyleItems
|
||||||
@ -70,87 +77,138 @@ func readStyle(runes []rune, defaultStyle Style) Style {
|
|||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this will start at ]( and look backwards to find the [ and forward
|
||||||
|
// to find the ) and record these Start and End indexes in a StyleBlock
|
||||||
|
func findStartEndOfStyle(pos int, runes []rune) StyleBlock {
|
||||||
|
current := pos
|
||||||
|
sb := StyleBlock{0, 0}
|
||||||
|
for {
|
||||||
|
current--
|
||||||
|
if runes[current] == tokenBeginStyledText {
|
||||||
|
sb.Start = current
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = pos
|
||||||
|
for {
|
||||||
|
current++
|
||||||
|
if runes[current] == tokenEndStyle {
|
||||||
|
sb.End = current
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
// if are string is "foo [thing](style) foo [more](style)"
|
||||||
|
// this will return "foo ", "[thing](style)", " foo ", "[more](style)"
|
||||||
|
func breakBlocksIntoStrings(s string) []string {
|
||||||
|
buff := []string{}
|
||||||
|
blocks := findStyleBlocks(s)
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
return buff
|
||||||
|
}
|
||||||
|
startEnd := len(s)
|
||||||
|
for i := len(blocks) - 1; i >= 0; i-- {
|
||||||
|
b := blocks[i]
|
||||||
|
item := s[b.End+1 : startEnd]
|
||||||
|
if item != "" {
|
||||||
|
buff = append([]string{item}, buff...)
|
||||||
|
}
|
||||||
|
item = s[b.Start : b.End+1]
|
||||||
|
buff = append([]string{item}, buff...)
|
||||||
|
startEnd = b.Start
|
||||||
|
}
|
||||||
|
item := s[0:startEnd]
|
||||||
|
if item != "" {
|
||||||
|
buff = append([]string{item}, buff...)
|
||||||
|
}
|
||||||
|
return buff
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through positions and make [] of StyleBlocks
|
||||||
|
func findStyleBlocks(s string) []StyleBlock {
|
||||||
|
items := []StyleBlock{}
|
||||||
|
runes := []rune(s)
|
||||||
|
|
||||||
|
positions := findStylePositions(s)
|
||||||
|
for _, pos := range positions {
|
||||||
|
sb := findStartEndOfStyle(pos, runes)
|
||||||
|
items = append(items, sb)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// uses tokenStyleKey ]( which tells us we have both a [text] and a (style)
|
||||||
|
// if are string is "foo [thing](style) foo [more](style)"
|
||||||
|
// this func will return a list of two ints: the index of the first ]( and
|
||||||
|
// the index of the next one
|
||||||
|
func findStylePositions(s string) []int {
|
||||||
|
index := strings.Index(s, tokenStyleKey)
|
||||||
|
if index == -1 {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
buff := []int{}
|
||||||
|
|
||||||
|
toProcess := s
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
buff = append(buff, index+offset)
|
||||||
|
toProcess = toProcess[index+1:]
|
||||||
|
offset += index + 1
|
||||||
|
index = strings.Index(toProcess, tokenStyleKey)
|
||||||
|
if index == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsStyle(s string) bool {
|
||||||
|
if strings.HasPrefix(s, string(tokenBeginStyledText)) &&
|
||||||
|
strings.HasSuffix(s, string(tokenEndStyle)) &&
|
||||||
|
strings.Contains(s, string(tokenEndStyledText)) &&
|
||||||
|
strings.Contains(s, string(tokenBeginStyle)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// [text](style) will return text
|
||||||
|
func extractTextFromBlock(item string) string {
|
||||||
|
index := strings.Index(item, string(tokenEndStyledText))
|
||||||
|
return item[1:index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// [text](style) will return style
|
||||||
|
func extractStyleFromBlock(item string) string {
|
||||||
|
index := strings.Index(item, string(tokenBeginStyle))
|
||||||
|
return item[index+1 : len(item)-1]
|
||||||
|
}
|
||||||
|
|
||||||
// ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling.
|
// ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling.
|
||||||
// Uses defaultStyle for any text without an embedded style.
|
// Uses defaultStyle for any text without an embedded style.
|
||||||
// Syntax is of the form [text](fg:<color>,mod:<attribute>,bg:<color>).
|
// Syntax is of the form [text](fg:<color>,mod:<attribute>,bg:<color>).
|
||||||
// Ordering does not matter. All fields are optional.
|
// Ordering does not matter. All fields are optional.
|
||||||
func ParseStyles(s string, defaultStyle Style) []Cell {
|
func ParseStyles(s string, defaultStyle Style) []Cell {
|
||||||
cells := []Cell{}
|
cells := []Cell{}
|
||||||
runes := []rune(s)
|
|
||||||
state := parserStateDefault
|
|
||||||
styledText := []rune{}
|
|
||||||
styleItems := []rune{}
|
|
||||||
squareCount := 0
|
|
||||||
|
|
||||||
reset := func() {
|
items := breakBlocksIntoStrings(s)
|
||||||
styledText = []rune{}
|
if len(items) == 0 {
|
||||||
styleItems = []rune{}
|
return RunesToStyledCells([]rune(s), defaultStyle)
|
||||||
state = parserStateDefault
|
|
||||||
squareCount = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rollback := func() {
|
for _, item := range items {
|
||||||
cells = append(cells, RunesToStyledCells(styledText, defaultStyle)...)
|
if containsStyle(item) {
|
||||||
cells = append(cells, RunesToStyledCells(styleItems, defaultStyle)...)
|
text := extractTextFromBlock(item)
|
||||||
reset()
|
styleText := extractStyleFromBlock(item)
|
||||||
}
|
style := readStyle([]rune(styleText), defaultStyle)
|
||||||
|
cells = append(cells, RunesToStyledCells([]rune(text), style)...)
|
||||||
// chop first and last runes
|
} else {
|
||||||
chop := func(s []rune) []rune {
|
cells = append(cells, RunesToStyledCells([]rune(item), defaultStyle)...)
|
||||||
return s[1 : len(s)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, _rune := range runes {
|
|
||||||
switch state {
|
|
||||||
case parserStateDefault:
|
|
||||||
if _rune == tokenBeginStyledText {
|
|
||||||
state = parserStateStyledText
|
|
||||||
squareCount = 1
|
|
||||||
styledText = append(styledText, _rune)
|
|
||||||
} else {
|
|
||||||
cells = append(cells, Cell{_rune, defaultStyle})
|
|
||||||
}
|
|
||||||
case parserStateStyledText:
|
|
||||||
switch {
|
|
||||||
case squareCount == 0:
|
|
||||||
switch _rune {
|
|
||||||
case tokenBeginStyle:
|
|
||||||
state = parserStateStyleItems
|
|
||||||
styleItems = append(styleItems, _rune)
|
|
||||||
default:
|
|
||||||
rollback()
|
|
||||||
switch _rune {
|
|
||||||
case tokenBeginStyledText:
|
|
||||||
state = parserStateStyledText
|
|
||||||
squareCount = 1
|
|
||||||
styleItems = append(styleItems, _rune)
|
|
||||||
default:
|
|
||||||
cells = append(cells, Cell{_rune, defaultStyle})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case len(runes) == i+1:
|
|
||||||
rollback()
|
|
||||||
styledText = append(styledText, _rune)
|
|
||||||
case _rune == tokenBeginStyledText:
|
|
||||||
squareCount++
|
|
||||||
styledText = append(styledText, _rune)
|
|
||||||
case _rune == tokenEndStyledText:
|
|
||||||
squareCount--
|
|
||||||
styledText = append(styledText, _rune)
|
|
||||||
default:
|
|
||||||
styledText = append(styledText, _rune)
|
|
||||||
}
|
|
||||||
case parserStateStyleItems:
|
|
||||||
styleItems = append(styleItems, _rune)
|
|
||||||
if _rune == tokenEndStyle {
|
|
||||||
style := readStyle(chop(styleItems), defaultStyle)
|
|
||||||
cells = append(cells, RunesToStyledCells(chop(styledText), style)...)
|
|
||||||
reset()
|
|
||||||
} else if len(runes) == i+1 {
|
|
||||||
rollback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cells
|
return cells
|
||||||
}
|
}
|
||||||
|
107
style_parser_test.go
Normal file
107
style_parser_test.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBreakBlocksIntoStrings(t *testing.T) {
|
||||||
|
items := breakBlocksIntoStrings("test [blue](fg:blue,mod:bold) and [red](fg:red) and maybe even [foo](bg:red)!")
|
||||||
|
if len(items) != 7 {
|
||||||
|
t.Fatal("wrong length", len(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindStylePositions(t *testing.T) {
|
||||||
|
items := findStylePositions("test [blue](fg:blue,mod:bold) and [red](fg:red) and maybe even [foo](bg:red)!")
|
||||||
|
if len(items) != 3 {
|
||||||
|
t.Fatal("wrong length", len(items))
|
||||||
|
}
|
||||||
|
if items[0] != 10 {
|
||||||
|
t.Fatal("wrong index", items[0])
|
||||||
|
}
|
||||||
|
if items[1] != 38 {
|
||||||
|
t.Fatal("wrong index", items[1])
|
||||||
|
}
|
||||||
|
if items[2] != 67 {
|
||||||
|
t.Fatal("wrong index", items[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindStyleBlocks(t *testing.T) {
|
||||||
|
items := findStyleBlocks("test [blue](fg:blue,mod:bold) and [red](fg:red) and maybe even [foo](bg:red)!")
|
||||||
|
if len(items) != 3 {
|
||||||
|
t.Fatal("wrong length", len(items))
|
||||||
|
}
|
||||||
|
if items[0].Start != 5 && items[0].End != 28 {
|
||||||
|
t.Fatal("wrong index", items[0])
|
||||||
|
}
|
||||||
|
if items[1].Start != 34 && items[1].End != 46 {
|
||||||
|
t.Fatal("wrong index", items[1])
|
||||||
|
}
|
||||||
|
if items[2].Start != 63 && items[2].End != 75 {
|
||||||
|
t.Fatal("wrong index", items[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseStyles(t *testing.T) {
|
||||||
|
cells := ParseStyles("test nothing", NewStyle(ColorWhite))
|
||||||
|
cells = ParseStyles("test [blue](fg:blue,bg:white,mod:bold) and [red](fg:red)", NewStyle(ColorWhite))
|
||||||
|
if len(cells) != 17 {
|
||||||
|
t.Fatal("wrong length", len(cells))
|
||||||
|
}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if cells[i].Style.Fg != ColorWhite {
|
||||||
|
t.Fatal("wrong fg color", cells[i], i)
|
||||||
|
}
|
||||||
|
if cells[i].Style.Bg != ColorClear {
|
||||||
|
t.Fatal("wrong bg color", cells[i])
|
||||||
|
}
|
||||||
|
if cells[i].Style.Modifier != ModifierClear {
|
||||||
|
t.Fatal("wrong mod", cells[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 5; i < 9; i++ {
|
||||||
|
if cells[i].Style.Fg != ColorBlue {
|
||||||
|
t.Fatal("wrong fg color", cells[i])
|
||||||
|
}
|
||||||
|
if cells[i].Style.Bg != ColorWhite {
|
||||||
|
t.Fatal("wrong bg color", cells[i])
|
||||||
|
}
|
||||||
|
if cells[i].Style.Modifier != ModifierBold {
|
||||||
|
t.Fatal("wrong mod", cells[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text := textFromCells(cells)
|
||||||
|
if text != "test blue and red" {
|
||||||
|
t.Fatal("wrong text", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
cells = ParseStyles("[blue](fg:blue) [1]", NewStyle(ColorWhite))
|
||||||
|
text = textFromCells(cells)
|
||||||
|
if text != "blue [1]" {
|
||||||
|
t.Fatal("wrong text", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
cells = ParseStyles("[0]", NewStyle(ColorWhite))
|
||||||
|
text = textFromCells(cells)
|
||||||
|
if text != "[0]" {
|
||||||
|
t.Fatal("wrong text", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
cells = ParseStyles("[", NewStyle(ColorWhite))
|
||||||
|
text = textFromCells(cells)
|
||||||
|
if text != "[" {
|
||||||
|
t.Fatal("wrong text", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFromCells(cells []Cell) string {
|
||||||
|
buff := []string{}
|
||||||
|
for _, cell := range cells {
|
||||||
|
buff = append(buff, string(cell.Rune))
|
||||||
|
}
|
||||||
|
return strings.Join(buff, "")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user