diff --git a/style_parser.go b/style_parser.go index ce537da..fd8214b 100644 --- a/style_parser.go +++ b/style_parser.go @@ -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:,mod:,bg:). // 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 } diff --git a/style_parser_test.go b/style_parser_test.go new file mode 100644 index 0000000..ce2dc29 --- /dev/null +++ b/style_parser_test.go @@ -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, "") +}