Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
59f7502
adding start of style_parser_test
andrewarrow Nov 5, 2022
a5730c9
adding failing test for [text] and no color info after
andrewarrow Nov 5, 2022
f047fb3
good failing tests showing the problem
andrewarrow Nov 7, 2022
2bd947e
fixing one test with wrong logic
andrewarrow Nov 7, 2022
94288e8
test to make sure current logic stays working
andrewarrow Nov 7, 2022
c4fdef9
very broken but just starting logic for new ParseStyles func
andrewarrow Nov 7, 2022
99a9b41
returning a styleString
andrewarrow Nov 7, 2022
4d1aaf1
adding new test for PrepareStyles
andrewarrow Nov 7, 2022
70b8f74
testing easy case of no style
andrewarrow Nov 7, 2022
90c992f
progress with parsing
andrewarrow Nov 7, 2022
22d0058
getting ready to link up styles
andrewarrow Nov 7, 2022
c77684f
progress with style
andrewarrow Nov 7, 2022
eaf7f8d
clean up and better order
andrewarrow Nov 7, 2022
f893b6f
more cleanup of printing
andrewarrow Nov 7, 2022
3be455e
broken but good new direction
andrewarrow Nov 8, 2022
4901307
StyleStringPosition struct helping
andrewarrow Nov 8, 2022
2c85cd9
better logic than split
andrewarrow Nov 8, 2022
291e957
moving to new FindStylePositions as first call
andrewarrow Nov 8, 2022
f9eac67
index checks for test
andrewarrow Nov 8, 2022
ea81b84
clean up and new broken test but with good indexes to hit
andrewarrow Nov 8, 2022
33fef6c
TestFindStyleBlocks is working
andrewarrow Nov 8, 2022
21dc21d
start of TestBreakBlocksIntoStrings
andrewarrow Nov 8, 2022
80c0896
logic breaking up blocks right
andrewarrow Nov 8, 2022
6cf9178
cleanup and printing result in test
andrewarrow Nov 8, 2022
55ae618
getting ready for final logic
andrewarrow Nov 8, 2022
29db86d
more cleanup and comments
andrewarrow Nov 9, 2022
2380572
adding containsStyle func
andrewarrow Nov 9, 2022
ecf4951
all tests passing
andrewarrow Nov 9, 2022
a7d7cdb
removing printlns
andrewarrow Nov 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 129 additions & 71 deletions style_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ const (

tokenBeginStyle = '('
tokenEndStyle = ')'

tokenStyleKey = "]("
)

type parserState uint

type StyleBlock struct {
Start int
End int
}

const (
parserStateDefault parserState = iota
parserStateStyleItems
Expand Down Expand Up @@ -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
}

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]
items := breakBlocksIntoStrings(s)
if len(items) == 0 {
return RunesToStyledCells([]rune(s), defaultStyle)
}

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 changes: 107 additions & 0 deletions style_parser_test.go
Original file line number Diff line number Diff line change
@@ -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, "")
}