1
0
mirror of https://github.com/gizak/termui.git synced 2025-04-24 13:48:50 +08:00

Merge a7d7cdbff3eaaa54d3f7002703be9959076813bb into 2b8f0c7960e9553acea6d579a740713066da5e13

This commit is contained in:
Andrew Arrow 2024-01-30 02:58:06 -08:00 committed by GitHub
commit d63aa965f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 236 additions and 71 deletions

View File

@ -21,10 +21,17 @@ const (
tokenBeginStyle = '('
tokenEndStyle = ')'
tokenStyleKey = "]("
)
type parserState uint
type StyleBlock struct {
Start int
End int
}
const (
parserStateDefault parserState = iota
parserStateStyleItems
@ -70,87 +77,138 @@ func readStyle(runes []rune, defaultStyle Style) 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.
// Uses defaultStyle for any text without an embedded style.
// Syntax is of the form [text](fg:<color>,mod:<attribute>,bg:<color>).
// Ordering does not matter. All fields are optional.
func ParseStyles(s string, defaultStyle Style) []Cell {
cells := []Cell{}
runes := []rune(s)
state := parserStateDefault
styledText := []rune{}
styleItems := []rune{}
squareCount := 0
reset := func() {
styledText = []rune{}
styleItems = []rune{}
state = parserStateDefault
squareCount = 0
items := breakBlocksIntoStrings(s)
if len(items) == 0 {
return RunesToStyledCells([]rune(s), defaultStyle)
}
rollback := func() {
cells = append(cells, RunesToStyledCells(styledText, defaultStyle)...)
cells = append(cells, RunesToStyledCells(styleItems, defaultStyle)...)
reset()
}
// chop first and last runes
chop := func(s []rune) []rune {
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()
}
for _, item := range items {
if containsStyle(item) {
text := extractTextFromBlock(item)
styleText := extractStyleFromBlock(item)
style := readStyle([]rune(styleText), defaultStyle)
cells = append(cells, RunesToStyledCells([]rune(text), style)...)
} else {
cells = append(cells, RunesToStyledCells([]rune(item), defaultStyle)...)
}
}
return cells
}

107
style_parser_test.go Normal file
View 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, "")
}